This commit is contained in:
sjk
2025-11-17 14:09:17 +08:00
commit 31e46c5bf6
479 changed files with 109324 additions and 0 deletions

View File

@@ -0,0 +1,357 @@
package handlers
import (
"net/http"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/common"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/middleware"
"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"
"github.com/go-playground/validator/v10"
)
type AuthHandler struct {
userService *services.UserService
validator *validator.Validate
}
func NewAuthHandler(userService *services.UserService) *AuthHandler {
return &AuthHandler{
userService: userService,
validator: validator.New(),
}
}
// RegisterRequest 注册请求结构
type RegisterRequest struct {
Username string `json:"username" validate:"required,min=3,max=20"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=6"`
Nickname string `json:"nickname" validate:"required,min=1,max=50"`
}
// LoginRequest 登录请求结构
type LoginRequest struct {
Account string `json:"account" validate:"required"` // 用户名或邮箱
Password string `json:"password" validate:"required"`
}
// RefreshTokenRequest 刷新令牌请求结构
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" validate:"required"`
}
// ChangePasswordRequest 修改密码请求结构
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" validate:"required"`
NewPassword string `json:"new_password" validate:"required,min=6"`
}
// AuthResponse 认证响应结构
type AuthResponse struct {
User *UserInfo `json:"user"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
}
// UserInfo 用户信息结构
type UserInfo struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Level string `json:"level"`
Status string `json:"status"`
}
// Register 用户注册
func (h *AuthHandler) Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ValidationErrorResponse(c, err)
return
}
// 验证请求参数
if err := h.validator.Struct(&req); err != nil {
common.ValidationErrorResponse(c, err)
return
}
// 验证邮箱格式
if !utils.IsValidEmail(req.Email) {
common.BadRequestResponse(c, "邮箱格式不正确")
return
}
// 验证密码强度
if !utils.IsStrongPassword(req.Password) {
common.BadRequestResponse(c, "密码强度不足至少8位且包含大小写字母、数字和特殊字符")
return
}
// 创建用户
user, err := h.userService.CreateUser(req.Username, req.Email, req.Password)
if err != nil {
if businessErr, ok := err.(*common.BusinessError); ok {
common.ErrorResponse(c, http.StatusBadRequest, businessErr.Message)
return
}
common.InternalServerErrorResponse(c, "用户创建失败")
return
}
// 生成令牌
accessToken, refreshToken, err := middleware.GenerateTokens(user.ID, user.Username, user.Email)
if err != nil {
common.InternalServerErrorResponse(c, "令牌生成失败")
return
}
// 更新用户登录信息
h.userService.UpdateLoginInfo(user.ID, utils.GetClientIP(c))
// 构造响应
nickname := ""
if user.Nickname != nil {
nickname = *user.Nickname
}
avatar := ""
if user.Avatar != nil {
avatar = *user.Avatar
}
userInfo := &UserInfo{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Nickname: nickname,
Avatar: avatar,
Level: "beginner",
Status: user.Status,
}
response := &AuthResponse{
User: userInfo,
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: 7200, // 2小时
}
common.SuccessResponse(c, response)
}
// Login 用户登录
func (h *AuthHandler) Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ValidationErrorResponse(c, err)
return
}
// 验证请求参数
if err := h.validator.Struct(&req); err != nil {
common.ValidationErrorResponse(c, err)
return
}
// 根据账号类型获取用户
var user *models.User
var err error
if utils.IsValidEmail(req.Account) {
user, err = h.userService.GetUserByEmail(req.Account)
} else {
user, err = h.userService.GetUserByUsername(req.Account)
}
if err != nil {
if businessErr, ok := err.(*common.BusinessError); ok {
common.ErrorResponse(c, http.StatusBadRequest, businessErr.Message)
return
}
common.InternalServerErrorResponse(c, "用户查询失败")
return
}
// 验证密码
if !utils.CheckPasswordHash(req.Password, user.PasswordHash) {
common.BadRequestResponse(c, "密码错误")
return
}
// 检查用户状态
if user.Status != "active" {
common.BadRequestResponse(c, "用户已被禁用")
return
}
// 生成令牌
accessToken, refreshToken, err := middleware.GenerateTokens(user.ID, user.Username, user.Email)
if err != nil {
common.InternalServerErrorResponse(c, "令牌生成失败")
return
}
// 更新用户登录信息
h.userService.UpdateLoginInfo(user.ID, utils.GetClientIP(c))
// 构造响应
nickname := ""
if user.Nickname != nil {
nickname = *user.Nickname
}
avatar := ""
if user.Avatar != nil {
avatar = *user.Avatar
}
userInfo := &UserInfo{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Nickname: nickname,
Avatar: avatar,
Level: "beginner",
Status: user.Status,
}
response := &AuthResponse{
User: userInfo,
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: 7200, // 2小时
}
common.SuccessResponse(c, response)
}
// RefreshToken 刷新访问令牌
func (h *AuthHandler) RefreshToken(c *gin.Context) {
var req RefreshTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ValidationErrorResponse(c, err)
return
}
// 验证刷新令牌
claims, err := middleware.ParseToken(req.RefreshToken)
if err != nil {
common.BadRequestResponse(c, "无效的刷新令牌")
return
}
// 检查令牌类型
if claims.Type != "refresh" {
common.BadRequestResponse(c, "令牌类型错误")
return
}
// 生成新的令牌
accessToken, newRefreshToken, err := middleware.GenerateTokens(claims.UserID, claims.Username, claims.Email)
if err != nil {
common.InternalServerErrorResponse(c, "令牌生成失败")
return
}
response := map[string]interface{}{
"access_token": accessToken,
"refresh_token": newRefreshToken,
"expires_in": 7200,
}
common.SuccessResponse(c, response)
}
// Logout 用户登出
func (h *AuthHandler) Logout(c *gin.Context) {
// 这里可以实现令牌黑名单机制
// 目前简单返回成功
common.SuccessResponse(c, map[string]string{"message": "登出成功"})
}
// GetProfile 获取用户资料
func (h *AuthHandler) GetProfile(c *gin.Context) {
// 获取当前用户ID
userID, exists := utils.GetUserIDFromContext(c)
if !exists {
common.BadRequestResponse(c, "请先登录")
return
}
user, err := h.userService.GetUserByID(userID)
if err != nil {
if businessErr, ok := err.(*common.BusinessError); ok {
common.ErrorResponse(c, http.StatusBadRequest, businessErr.Message)
return
}
common.InternalServerErrorResponse(c, "用户查询失败")
return
}
nickname := ""
if user.Nickname != nil {
nickname = *user.Nickname
}
avatar := ""
if user.Avatar != nil {
avatar = *user.Avatar
}
userInfo := &UserInfo{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Nickname: nickname,
Avatar: avatar,
Level: "beginner",
Status: user.Status,
}
common.SuccessResponse(c, userInfo)
}
// ChangePassword 修改密码
func (h *AuthHandler) ChangePassword(c *gin.Context) {
// 获取当前用户ID
userID, exists := utils.GetUserIDFromContext(c)
if !exists {
common.BadRequestResponse(c, "请先登录")
return
}
var req ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ValidationErrorResponse(c, err)
return
}
// 验证请求参数
if err := h.validator.Struct(&req); err != nil {
common.ValidationErrorResponse(c, err)
return
}
// 验证新密码强度
if !utils.IsStrongPassword(req.NewPassword) {
common.BadRequestResponse(c, "新密码强度不够")
return
}
// 更新密码
err := h.userService.UpdatePassword(userID, req.OldPassword, req.NewPassword)
if err != nil {
if businessErr, ok := err.(*common.BusinessError); ok {
common.ErrorResponse(c, http.StatusBadRequest, businessErr.Message)
return
}
common.InternalServerErrorResponse(c, "密码更新失败")
return
}
common.SuccessResponse(c, map[string]string{"message": "密码修改成功"})
}

View File

@@ -0,0 +1,134 @@
package handlers
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/config"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/database"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/common"
)
// HealthResponse 健康检查响应结构
type HealthResponse struct {
Status string `json:"status"`
Timestamp time.Time `json:"timestamp"`
Version string `json:"version"`
Services map[string]string `json:"services"`
}
// VersionResponse 版本信息响应结构
type VersionResponse struct {
Name string `json:"name"`
Version string `json:"version"`
Environment string `json:"environment"`
BuildTime string `json:"build_time"`
}
// HealthCheck 健康检查端点
func HealthCheck(c *gin.Context) {
services := make(map[string]string)
// 检查数据库连接
db := database.GetDB()
if db != nil {
sqlDB, err := db.DB()
if err != nil {
services["database"] = "error"
} else {
if err := sqlDB.Ping(); err != nil {
services["database"] = "down"
} else {
services["database"] = "up"
}
}
} else {
services["database"] = "not_initialized"
}
// 检查Redis连接如果配置了Redis
// TODO: 添加Redis健康检查
services["redis"] = "not_implemented"
// 确定整体状态
status := "healthy"
for _, serviceStatus := range services {
if serviceStatus != "up" && serviceStatus != "not_implemented" {
status = "unhealthy"
break
}
}
response := HealthResponse{
Status: status,
Timestamp: time.Now(),
Version: config.GlobalConfig.App.Version,
Services: services,
}
if status == "healthy" {
common.SuccessResponse(c, response)
} else {
c.JSON(http.StatusServiceUnavailable, gin.H{
"code": http.StatusServiceUnavailable,
"message": "Service unhealthy",
"data": response,
})
}
}
// GetVersion 获取版本信息
func GetVersion(c *gin.Context) {
response := VersionResponse{
Name: config.GlobalConfig.App.Name,
Version: config.GlobalConfig.App.Version,
Environment: config.GlobalConfig.App.Environment,
BuildTime: time.Now().Format("2006-01-02 15:04:05"), // 实际项目中应该在编译时注入
}
common.SuccessResponse(c, response)
}
// ReadinessCheck 就绪检查端点用于Kubernetes等容器编排
func ReadinessCheck(c *gin.Context) {
// 检查关键服务是否就绪
db := database.GetDB()
if db == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "not_ready",
"reason": "database_not_initialized",
})
return
}
sqlDB, err := db.DB()
if err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "not_ready",
"reason": "database_connection_error",
})
return
}
if err := sqlDB.Ping(); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "not_ready",
"reason": "database_ping_failed",
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "ready",
})
}
// LivenessCheck 存活检查端点用于Kubernetes等容器编排
func LivenessCheck(c *gin.Context) {
// 简单的存活检查,只要服务能响应就认为是存活的
c.JSON(http.StatusOK, gin.H{
"status": "alive",
"timestamp": time.Now(),
})
}

View File

@@ -0,0 +1,15 @@
// api/handlers/hello_handler.go
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
)
// HelloHandler 处理 /hello 请求
func HelloHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Hello from a structured project!",
})
}

View File

@@ -0,0 +1,593 @@
package handlers
import (
"net/http"
"strconv"
"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/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
// ListeningHandler 听力训练处理器
type ListeningHandler struct {
listeningService *services.ListeningService
validate *validator.Validate
}
// NewListeningHandler 创建听力训练处理器实例
func NewListeningHandler(listeningService *services.ListeningService, validate *validator.Validate) *ListeningHandler {
return &ListeningHandler{
listeningService: listeningService,
validate: validate,
}
}
func getUserIDString(c *gin.Context) (string, bool) {
uid, exists := c.Get("user_id")
if !exists || uid == nil {
return "", false
}
switch v := uid.(type) {
case string:
return v, true
case int:
return strconv.Itoa(v), true
case int64:
return strconv.FormatInt(v, 10), true
default:
return "", false
}
}
// CreateMaterialRequest 创建听力材料请求
type CreateMaterialRequest struct {
Title string `json:"title" validate:"required,max=200"`
Description *string `json:"description"`
AudioURL string `json:"audio_url" validate:"required,url,max=500"`
Transcript *string `json:"transcript"`
Duration int `json:"duration" validate:"min=0"`
Level string `json:"level" validate:"required,oneof=beginner intermediate advanced"`
Category string `json:"category" validate:"max=50"`
Tags *string `json:"tags"`
}
// UpdateMaterialRequest 更新听力材料请求
type UpdateMaterialRequest struct {
Title *string `json:"title" validate:"omitempty,max=200"`
Description *string `json:"description"`
AudioURL *string `json:"audio_url" validate:"omitempty,url,max=500"`
Transcript *string `json:"transcript"`
Duration *int `json:"duration" validate:"omitempty,min=0"`
Level *string `json:"level" validate:"omitempty,oneof=beginner intermediate advanced"`
Category *string `json:"category" validate:"omitempty,max=50"`
Tags *string `json:"tags"`
}
// CreateRecordRequest 创建听力练习记录请求
type CreateRecordRequest struct {
MaterialID string `json:"material_id" validate:"required"`
}
// UpdateRecordRequest 更新听力练习记录请求
type UpdateRecordRequest struct {
Score *float64 `json:"score" validate:"omitempty,min=0,max=100"`
Accuracy *float64 `json:"accuracy" validate:"omitempty,min=0,max=100"`
CompletionRate *float64 `json:"completion_rate" validate:"omitempty,min=0,max=100"`
TimeSpent *int `json:"time_spent" validate:"omitempty,min=0"`
Answers *string `json:"answers"`
Feedback *string `json:"feedback"`
Completed *bool `json:"completed"`
}
// GetListeningMaterials 获取听力材料列表
func (h *ListeningHandler) GetListeningMaterials(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
}
materials, total, err := h.listeningService.GetListeningMaterials(level, category, page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "获取听力材料失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "获取成功",
"data": gin.H{
"materials": materials,
"total": total,
"page": page,
"page_size": pageSize,
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
},
})
}
// GetListeningMaterial 获取单个听力材料
func (h *ListeningHandler) GetListeningMaterial(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "材料ID不能为空",
})
return
}
material, err := h.listeningService.GetListeningMaterial(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "获取成功",
"data": material,
})
}
// CreateListeningMaterial 创建听力材料
func (h *ListeningHandler) CreateListeningMaterial(c *gin.Context) {
var req CreateMaterialRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "请求参数错误",
"error": err.Error(),
})
return
}
if err := h.validate.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数验证失败",
"error": err.Error(),
})
return
}
material := &models.ListeningMaterial{
Title: req.Title,
Description: req.Description,
AudioURL: req.AudioURL,
Transcript: req.Transcript,
Duration: req.Duration,
Level: req.Level,
Category: req.Category,
Tags: req.Tags,
}
if err := h.listeningService.CreateListeningMaterial(material); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "创建听力材料失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"code": 201,
"message": "创建成功",
"data": material,
})
}
// UpdateListeningMaterial 更新听力材料
func (h *ListeningHandler) UpdateListeningMaterial(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "材料ID不能为空",
})
return
}
var req UpdateMaterialRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "请求参数错误",
"error": err.Error(),
})
return
}
if err := h.validate.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数验证失败",
"error": err.Error(),
})
return
}
updates := make(map[string]interface{})
if req.Title != nil {
updates["title"] = *req.Title
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.AudioURL != nil {
updates["audio_url"] = *req.AudioURL
}
if req.Transcript != nil {
updates["transcript"] = *req.Transcript
}
if req.Duration != nil {
updates["duration"] = *req.Duration
}
if req.Level != nil {
updates["level"] = *req.Level
}
if req.Category != nil {
updates["category"] = *req.Category
}
if req.Tags != nil {
updates["tags"] = *req.Tags
}
if err := h.listeningService.UpdateListeningMaterial(id, updates); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "更新听力材料失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "更新成功",
})
}
// DeleteListeningMaterial 删除听力材料
func (h *ListeningHandler) DeleteListeningMaterial(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "材料ID不能为空",
})
return
}
if err := h.listeningService.DeleteListeningMaterial(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "删除听力材料失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "删除成功",
})
}
// SearchListeningMaterials 搜索听力材料
func (h *ListeningHandler) SearchListeningMaterials(c *gin.Context) {
keyword := c.Query("q")
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
}
materials, total, err := h.listeningService.SearchListeningMaterials(keyword, level, category, page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "搜索听力材料失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "搜索成功",
"data": gin.H{
"materials": materials,
"total": total,
"page": page,
"page_size": pageSize,
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
},
})
}
// CreateListeningRecord 创建听力练习记录
func (h *ListeningHandler) CreateListeningRecord(c *gin.Context) {
userIDStr, ok := getUserIDString(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "未授权访问",
})
return
}
var req CreateRecordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "请求参数错误",
"error": err.Error(),
})
return
}
if err := h.validate.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数验证失败",
"error": err.Error(),
})
return
}
record := &models.ListeningRecord{
UserID: userIDStr,
MaterialID: req.MaterialID,
}
if err := h.listeningService.CreateListeningRecord(record); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "创建听力练习记录失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"code": 201,
"message": "创建成功",
"data": record,
})
}
// UpdateListeningRecord 更新听力练习记录
func (h *ListeningHandler) UpdateListeningRecord(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "记录ID不能为空",
})
return
}
var req UpdateRecordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "请求参数错误",
"error": err.Error(),
})
return
}
if err := h.validate.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数验证失败",
"error": err.Error(),
})
return
}
updates := make(map[string]interface{})
if req.Score != nil {
updates["score"] = *req.Score
}
if req.Accuracy != nil {
updates["accuracy"] = *req.Accuracy
}
if req.CompletionRate != nil {
updates["completion_rate"] = *req.CompletionRate
}
if req.TimeSpent != nil {
updates["time_spent"] = *req.TimeSpent
}
if req.Answers != nil {
updates["answers"] = *req.Answers
}
if req.Feedback != nil {
updates["feedback"] = *req.Feedback
}
if req.Completed != nil && *req.Completed {
updates["completed_at"] = true // 这会在service中被处理为实际时间
}
if err := h.listeningService.UpdateListeningRecord(id, updates); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "更新听力练习记录失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "更新成功",
})
}
// GetUserListeningRecords 获取用户听力练习记录
func (h *ListeningHandler) GetUserListeningRecords(c *gin.Context) {
userIDStr, ok := getUserIDString(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "未授权访问",
})
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
}
records, total, err := h.listeningService.GetUserListeningRecords(userIDStr, page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "获取听力练习记录失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "获取成功",
"data": gin.H{
"records": records,
"total": total,
"page": page,
"page_size": pageSize,
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
},
})
}
// GetListeningRecord 获取单个听力练习记录
func (h *ListeningHandler) GetListeningRecord(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "记录ID不能为空",
})
return
}
record, err := h.listeningService.GetListeningRecord(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "获取成功",
"data": record,
})
}
// GetUserListeningStats 获取用户听力学习统计
func (h *ListeningHandler) GetUserListeningStats(c *gin.Context) {
userIDStr, ok := getUserIDString(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "未授权访问",
})
return
}
stats, err := h.listeningService.GetUserListeningStats(userIDStr)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "获取听力学习统计失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "获取成功",
"data": stats,
})
}
// GetListeningProgress 获取用户在特定材料上的学习进度
func (h *ListeningHandler) GetListeningProgress(c *gin.Context) {
userIDStr, ok := getUserIDString(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "未授权访问",
})
return
}
materialID := c.Param("material_id")
if materialID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "材料ID不能为空",
})
return
}
progress, err := h.listeningService.GetListeningProgress(userIDStr, materialID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "获取学习进度失败",
"error": err.Error(),
})
return
}
if progress == nil {
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "暂无学习记录",
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "获取成功",
"data": progress,
})
}

View File

@@ -0,0 +1,639 @@
package handlers
import (
"net/http"
"strconv"
"time"
"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"
)
// ReadingHandler 阅读理解处理器
type ReadingHandler struct {
readingService *services.ReadingService
}
// NewReadingHandler 创建阅读理解处理器实例
func NewReadingHandler(readingService *services.ReadingService) *ReadingHandler {
return &ReadingHandler{
readingService: readingService,
}
}
// ===== 请求结构体定义 =====
// CreateReadingMaterialRequest 创建阅读材料请求
type CreateReadingMaterialRequest struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
Summary string `json:"summary"`
Level string `json:"level" binding:"required,oneof=beginner intermediate advanced"`
Category string `json:"category" binding:"required"`
WordCount int `json:"word_count"`
Tags string `json:"tags"`
Source string `json:"source"`
Author string `json:"author"`
}
// UpdateReadingMaterialRequest 更新阅读材料请求
type UpdateReadingMaterialRequest struct {
Title *string `json:"title"`
Content *string `json:"content"`
Summary *string `json:"summary"`
Level *string `json:"level"`
Category *string `json:"category"`
WordCount *int `json:"word_count"`
Tags *string `json:"tags"`
Source *string `json:"source"`
Author *string `json:"author"`
}
// CreateReadingRecordRequest 创建阅读记录请求
type CreateReadingRecordRequest struct {
MaterialID string `json:"material_id" binding:"required"`
}
// UpdateReadingRecordRequest 更新阅读记录请求
type UpdateReadingRecordRequest struct {
ReadingTime *int `json:"reading_time"`
ComprehensionScore *float64 `json:"comprehension_score"`
ReadingSpeed *float64 `json:"reading_speed"`
Notes *string `json:"notes"`
CompletedAt *time.Time `json:"completed_at"`
}
// ===== 阅读材料管理接口 =====
// GetReadingMaterials 获取阅读材料列表
// @Summary 获取阅读材料列表
// @Description 获取阅读材料列表,支持按难度级别和分类筛选
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Param level query string false "难度级别"
// @Param category query string false "分类"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Success 200 {object} Response
// @Router /reading/materials [get]
func (h *ReadingHandler) GetReadingMaterials(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", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
materials, total, err := h.readingService.GetReadingMaterials(level, category, page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取阅读材料失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": materials,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
},
})
}
// GetReadingMaterial 获取单个阅读材料
// @Summary 获取单个阅读材料
// @Description 根据ID获取阅读材料详情
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Param id path string true "材料ID"
// @Success 200 {object} Response
// @Router /reading/materials/{id} [get]
func (h *ReadingHandler) GetReadingMaterial(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "材料ID不能为空"})
return
}
material, err := h.readingService.GetReadingMaterial(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "阅读材料不存在",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{"data": material})
}
// CreateReadingMaterial 创建阅读材料
// @Summary 创建阅读材料
// @Description 创建新的阅读材料
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Param request body CreateReadingMaterialRequest true "创建请求"
// @Success 201 {object} Response
// @Router /reading/materials [post]
func (h *ReadingHandler) CreateReadingMaterial(c *gin.Context) {
var req CreateReadingMaterialRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
material := &models.ReadingMaterial{
Title: req.Title,
Content: req.Content,
Summary: &req.Summary,
WordCount: req.WordCount,
Level: req.Level,
Category: req.Category,
Tags: &req.Tags,
Source: &req.Source,
Author: &req.Author,
IsActive: true,
}
if err := h.readingService.CreateReadingMaterial(material); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "创建阅读材料失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "阅读材料创建成功",
"data": material,
})
}
// UpdateReadingMaterial 更新阅读材料
// @Summary 更新阅读材料
// @Description 更新阅读材料信息
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Param id path string true "材料ID"
// @Param request body UpdateReadingMaterialRequest true "更新请求"
// @Success 200 {object} Response
// @Router /reading/materials/{id} [put]
func (h *ReadingHandler) UpdateReadingMaterial(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "材料ID不能为空"})
return
}
var req UpdateReadingMaterialRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
updates := make(map[string]interface{})
if req.Title != nil {
updates["title"] = *req.Title
}
if req.Content != nil {
updates["content"] = *req.Content
}
if req.Summary != nil {
updates["summary"] = *req.Summary
}
if req.Level != nil {
updates["level"] = *req.Level
}
if req.Category != nil {
updates["category"] = *req.Category
}
if req.WordCount != nil {
updates["word_count"] = *req.WordCount
}
if req.Tags != nil {
updates["tags"] = *req.Tags
}
if req.Source != nil {
updates["source"] = *req.Source
}
if req.Author != nil {
updates["author"] = *req.Author
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "没有提供更新字段"})
return
}
if err := h.readingService.UpdateReadingMaterial(id, updates); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "更新阅读材料失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{"message": "阅读材料更新成功"})
}
// DeleteReadingMaterial 删除阅读材料
// @Summary 删除阅读材料
// @Description 软删除阅读材料
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Param id path string true "材料ID"
// @Success 200 {object} Response
// @Router /reading/materials/{id} [delete]
func (h *ReadingHandler) DeleteReadingMaterial(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "材料ID不能为空"})
return
}
if err := h.readingService.DeleteReadingMaterial(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "删除阅读材料失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{"message": "阅读材料删除成功"})
}
// SearchReadingMaterials 搜索阅读材料
// @Summary 搜索阅读材料
// @Description 根据关键词搜索阅读材料
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Param keyword query string true "搜索关键词"
// @Param level query string false "难度级别"
// @Param category query string false "分类"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Success 200 {object} Response
// @Router /reading/materials/search [get]
func (h *ReadingHandler) SearchReadingMaterials(c *gin.Context) {
keyword := c.Query("keyword")
if keyword == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "搜索关键词不能为空"})
return
}
level := c.Query("level")
category := c.Query("category")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
materials, total, err := h.readingService.SearchReadingMaterials(keyword, level, category, page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "搜索阅读材料失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": materials,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
},
})
}
// ===== 阅读记录管理接口 =====
// CreateReadingRecord 创建阅读记录
// @Summary 创建阅读记录
// @Description 开始阅读材料,创建阅读记录
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Param request body CreateReadingRecordRequest true "创建请求"
// @Success 201 {object} Response
// @Router /reading/records [post]
func (h *ReadingHandler) CreateReadingRecord(c *gin.Context) {
userID, ok := utils.GetUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未认证"})
return
}
var req CreateReadingRecordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
// 检查是否已有该材料的阅读记录
existingRecord, err := h.readingService.GetReadingProgress(utils.Int64ToString(userID), req.MaterialID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "检查阅读记录失败",
"details": err.Error(),
})
return
}
if existingRecord != nil {
c.JSON(http.StatusOK, gin.H{
"message": "阅读记录已存在",
"data": existingRecord,
})
return
}
record := &models.ReadingRecord{
UserID: utils.Int64ToString(userID),
MaterialID: req.MaterialID,
}
if err := h.readingService.CreateReadingRecord(record); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "创建阅读记录失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "阅读记录创建成功",
"data": record,
})
}
// UpdateReadingRecord 更新阅读记录
// @Summary 更新阅读记录
// @Description 更新阅读进度和成绩
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Param id path string true "记录ID"
// @Param request body UpdateReadingRecordRequest true "更新请求"
// @Success 200 {object} Response
// @Router /reading/records/{id} [put]
func (h *ReadingHandler) UpdateReadingRecord(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "记录ID不能为空"})
return
}
var req UpdateReadingRecordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
updates := make(map[string]interface{})
if req.ReadingTime != nil {
updates["reading_time"] = *req.ReadingTime
}
if req.ComprehensionScore != nil {
updates["comprehension_score"] = *req.ComprehensionScore
}
if req.ReadingSpeed != nil {
updates["reading_speed"] = *req.ReadingSpeed
}
if req.Notes != nil {
updates["notes"] = *req.Notes
}
if req.CompletedAt != nil {
updates["completed_at"] = *req.CompletedAt
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "没有提供更新字段"})
return
}
if err := h.readingService.UpdateReadingRecord(id, updates); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "更新阅读记录失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{"message": "阅读记录更新成功"})
}
// GetUserReadingRecords 获取用户阅读记录
// @Summary 获取用户阅读记录
// @Description 获取当前用户的阅读记录列表
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Success 200 {object} Response
// @Router /reading/records [get]
func (h *ReadingHandler) GetUserReadingRecords(c *gin.Context) {
userID, ok := utils.GetUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未认证"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
records, total, err := h.readingService.GetUserReadingRecords(utils.Int64ToString(userID), page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取阅读记录失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": records,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
},
})
}
// GetReadingRecord 获取单个阅读记录
// @Summary 获取单个阅读记录
// @Description 根据ID获取阅读记录详情
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Param id path string true "记录ID"
// @Success 200 {object} Response
// @Router /reading/records/{id} [get]
func (h *ReadingHandler) GetReadingRecord(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "记录ID不能为空"})
return
}
record, err := h.readingService.GetReadingRecord(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "阅读记录不存在",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{"data": record})
}
// GetReadingProgress 获取阅读进度
// @Summary 获取阅读进度
// @Description 获取用户对特定材料的阅读进度
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Param material_id path string true "材料ID"
// @Success 200 {object} Response
// @Router /reading/progress/{material_id} [get]
func (h *ReadingHandler) GetReadingProgress(c *gin.Context) {
userID, ok := utils.GetUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未认证"})
return
}
materialID := c.Param("material_id")
if materialID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "材料ID不能为空"})
return
}
record, err := h.readingService.GetReadingProgress(utils.Int64ToString(userID), materialID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取阅读进度失败",
"details": err.Error(),
})
return
}
if record == nil {
c.JSON(http.StatusOK, gin.H{
"data": nil,
"message": "暂无阅读记录",
})
return
}
c.JSON(http.StatusOK, gin.H{"data": record})
}
// ===== 阅读统计接口 =====
// GetReadingStats 获取阅读统计
// @Summary 获取阅读统计
// @Description 获取用户阅读统计信息
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Success 200 {object} Response
// @Router /reading/stats [get]
func (h *ReadingHandler) GetReadingStats(c *gin.Context) {
userID, ok := utils.GetUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未认证"})
return
}
stats, err := h.readingService.GetUserReadingStats(utils.Int64ToString(userID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取阅读统计失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{"data": stats})
}
// GetRecommendedMaterials 获取推荐阅读材料
// @Summary 获取推荐阅读材料
// @Description 根据用户阅读历史推荐合适的阅读材料
// @Tags 阅读理解
// @Accept json
// @Produce json
// @Param limit query int false "推荐数量" default(5)
// @Success 200 {object} Response
// @Router /reading/recommendations [get]
func (h *ReadingHandler) GetRecommendedMaterials(c *gin.Context) {
userID, ok := utils.GetUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未认证"})
return
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "5"))
if limit < 1 || limit > 20 {
limit = 5
}
materials, err := h.readingService.GetRecommendedMaterials(utils.Int64ToString(userID), limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取推荐材料失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{"data": materials})
}

View File

@@ -0,0 +1,449 @@
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)
}

View File

@@ -0,0 +1,419 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"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"
)
// TestHandler 测试处理器
type TestHandler struct {
testService *services.TestService
}
// NewTestHandler 创建测试处理器实例
func NewTestHandler(testService *services.TestService) *TestHandler {
return &TestHandler{
testService: testService,
}
}
// GetTestTemplates 获取测试模板列表
// @Summary 获取测试模板列表
// @Tags Test
// @Param type query string false "测试类型"
// @Param difficulty query string false "难度"
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} common.Response
// @Router /api/v1/tests/templates [get]
func (h *TestHandler) GetTestTemplates(c *gin.Context) {
typeStr := c.Query("type")
difficultyStr := c.Query("difficulty")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
var testType *models.TestType
if typeStr != "" {
t := models.TestType(typeStr)
testType = &t
}
var difficulty *models.TestDifficulty
if difficultyStr != "" {
d := models.TestDifficulty(difficultyStr)
difficulty = &d
}
templates, total, err := h.testService.GetTestTemplates(testType, difficulty, page, pageSize)
if err != nil {
common.ErrorResponse(c, http.StatusInternalServerError, "获取测试模板失败")
return
}
common.SuccessResponse(c, gin.H{
"templates": templates,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
},
})
}
// GetTestTemplateByID 获取测试模板详情
// @Summary 获取测试模板详情
// @Tags Test
// @Param id path string true "模板ID"
// @Success 200 {object} common.Response
// @Router /api/v1/tests/templates/{id} [get]
func (h *TestHandler) GetTestTemplateByID(c *gin.Context) {
id := c.Param("id")
template, err := h.testService.GetTestTemplateByID(id)
if err != nil {
common.ErrorResponse(c, http.StatusNotFound, "测试模板不存在")
return
}
common.SuccessResponse(c, gin.H{"data": template})
}
// CreateTestSession 创建测试会话
// @Summary 创建测试会话
// @Tags Test
// @Param body body object true "请求体"
// @Success 201 {object} common.Response
// @Router /api/v1/tests/sessions [post]
func (h *TestHandler) CreateTestSession(c *gin.Context) {
userID, exists := utils.GetUserIDFromContext(c)
if !exists {
common.BadRequestResponse(c, "请先登录")
return
}
var req struct {
TemplateID string `json:"template_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
common.BadRequestResponse(c, "请求参数错误")
return
}
session, err := h.testService.CreateTestSession(req.TemplateID, utils.Int64ToString(userID))
if err != nil {
common.ErrorResponse(c, http.StatusInternalServerError, "创建测试会话失败: "+err.Error())
return
}
common.SuccessResponseWithStatus(c, http.StatusCreated, gin.H{"data": session})
}
// GetTestSession 获取测试会话
// @Summary 获取测试会话
// @Tags Test
// @Param id path string true "会话ID"
// @Success 200 {object} common.Response
// @Router /api/v1/tests/sessions/{id} [get]
func (h *TestHandler) GetTestSession(c *gin.Context) {
sessionID := c.Param("id")
session, err := h.testService.GetTestSession(sessionID)
if err != nil {
common.ErrorResponse(c, http.StatusNotFound, "测试会话不存在")
return
}
// 验证用户权限
userID, _ := utils.GetUserIDFromContext(c)
if session.UserID != utils.Int64ToString(userID) {
common.ErrorResponse(c, http.StatusForbidden, "无权访问此测试会话")
return
}
common.SuccessResponse(c, gin.H{"data": session})
}
// StartTest 开始测试
// @Summary 开始测试
// @Tags Test
// @Param id path string true "会话ID"
// @Success 200 {object} common.Response
// @Router /api/v1/tests/sessions/{id}/start [put]
func (h *TestHandler) StartTest(c *gin.Context) {
sessionID := c.Param("id")
// 验证用户权限
session, err := h.testService.GetTestSession(sessionID)
if err != nil {
common.ErrorResponse(c, http.StatusNotFound, "测试会话不存在")
return
}
userID, _ := utils.GetUserIDFromContext(c)
if session.UserID != utils.Int64ToString(userID) {
common.ErrorResponse(c, http.StatusForbidden, "无权操作此测试会话")
return
}
updatedSession, err := h.testService.StartTest(sessionID)
if err != nil {
common.ErrorResponse(c, http.StatusBadRequest, err.Error())
return
}
common.SuccessResponse(c, gin.H{"data": updatedSession})
}
// SubmitAnswer 提交答案
// @Summary 提交答案
// @Tags Test
// @Param id path string true "会话ID"
// @Param body body object true "请求体"
// @Success 200 {object} common.Response
// @Router /api/v1/tests/sessions/{id}/answers [post]
func (h *TestHandler) SubmitAnswer(c *gin.Context) {
sessionID := c.Param("id")
var req struct {
QuestionID string `json:"question_id" binding:"required"`
Answer string `json:"answer" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
common.BadRequestResponse(c, "请求参数错误")
return
}
// 验证用户权限
session, err := h.testService.GetTestSession(sessionID)
if err != nil {
common.ErrorResponse(c, http.StatusNotFound, "测试会话不存在")
return
}
userID, _ := utils.GetUserIDFromContext(c)
if session.UserID != utils.Int64ToString(userID) {
common.ErrorResponse(c, http.StatusForbidden, "无权操作此测试会话")
return
}
updatedSession, err := h.testService.SubmitAnswer(sessionID, req.QuestionID, req.Answer)
if err != nil {
common.ErrorResponse(c, http.StatusBadRequest, err.Error())
return
}
common.SuccessResponse(c, gin.H{"data": updatedSession})
}
// PauseTest 暂停测试
// @Summary 暂停测试
// @Tags Test
// @Param id path string true "会话ID"
// @Success 200 {object} common.Response
// @Router /api/v1/tests/sessions/{id}/pause [put]
func (h *TestHandler) PauseTest(c *gin.Context) {
sessionID := c.Param("id")
// 验证用户权限
session, err := h.testService.GetTestSession(sessionID)
if err != nil {
common.ErrorResponse(c, http.StatusNotFound, "测试会话不存在")
return
}
userID, _ := utils.GetUserIDFromContext(c)
if session.UserID != utils.Int64ToString(userID) {
common.ErrorResponse(c, http.StatusForbidden, "无权操作此测试会话")
return
}
updatedSession, err := h.testService.PauseTest(sessionID)
if err != nil {
common.ErrorResponse(c, http.StatusBadRequest, err.Error())
return
}
common.SuccessResponse(c, gin.H{"data": updatedSession})
}
// ResumeTest 恢复测试
// @Summary 恢复测试
// @Tags Test
// @Param id path string true "会话ID"
// @Success 200 {object} common.Response
// @Router /api/v1/tests/sessions/{id}/resume [put]
func (h *TestHandler) ResumeTest(c *gin.Context) {
sessionID := c.Param("id")
// 验证用户权限
session, err := h.testService.GetTestSession(sessionID)
if err != nil {
common.ErrorResponse(c, http.StatusNotFound, "测试会话不存在")
return
}
userID, _ := utils.GetUserIDFromContext(c)
if session.UserID != utils.Int64ToString(userID) {
common.ErrorResponse(c, http.StatusForbidden, "无权操作此测试会话")
return
}
updatedSession, err := h.testService.ResumeTest(sessionID)
if err != nil {
common.ErrorResponse(c, http.StatusBadRequest, err.Error())
return
}
common.SuccessResponse(c, gin.H{"data": updatedSession})
}
// CompleteTest 完成测试
// @Summary 完成测试
// @Tags Test
// @Param id path string true "会话ID"
// @Success 200 {object} common.Response
// @Router /api/v1/tests/sessions/{id}/complete [put]
func (h *TestHandler) CompleteTest(c *gin.Context) {
sessionID := c.Param("id")
// 验证用户权限
session, err := h.testService.GetTestSession(sessionID)
if err != nil {
common.ErrorResponse(c, http.StatusNotFound, "测试会话不存在")
return
}
userID, _ := utils.GetUserIDFromContext(c)
if session.UserID != utils.Int64ToString(userID) {
common.ErrorResponse(c, http.StatusForbidden, "无权操作此测试会话")
return
}
result, err := h.testService.CompleteTest(sessionID)
if err != nil {
common.ErrorResponse(c, http.StatusBadRequest, err.Error())
return
}
common.SuccessResponse(c, gin.H{"data": result})
}
// GetUserTestHistory 获取用户测试历史
// @Summary 获取用户测试历史
// @Tags Test
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} common.Response
// @Router /api/v1/tests/sessions [get]
func (h *TestHandler) GetUserTestHistory(c *gin.Context) {
userID, exists := utils.GetUserIDFromContext(c)
if !exists {
common.BadRequestResponse(c, "请先登录")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
results, total, err := h.testService.GetUserTestHistory(utils.Int64ToString(userID), page, pageSize)
if err != nil {
common.ErrorResponse(c, http.StatusInternalServerError, "获取测试历史失败")
return
}
common.SuccessResponse(c, gin.H{
"sessions": results,
"pagination": gin.H{
"page": page,
"page_size": pageSize,
"total": total,
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
},
})
}
// GetTestResultByID 获取测试结果详情
// @Summary 获取测试结果详情
// @Tags Test
// @Param id path string true "结果ID"
// @Success 200 {object} common.Response
// @Router /api/v1/tests/results/{id} [get]
func (h *TestHandler) GetTestResultByID(c *gin.Context) {
resultID := c.Param("id")
result, err := h.testService.GetTestResultByID(resultID)
if err != nil {
common.ErrorResponse(c, http.StatusNotFound, "测试结果不存在")
return
}
// 验证用户权限
userID, _ := utils.GetUserIDFromContext(c)
if result.UserID != utils.Int64ToString(userID) {
common.ErrorResponse(c, http.StatusForbidden, "无权访问此测试结果")
return
}
common.SuccessResponse(c, gin.H{"data": result})
}
// GetUserTestStats 获取用户测试统计
// @Summary 获取用户测试统计
// @Tags Test
// @Success 200 {object} common.Response
// @Router /api/v1/tests/stats [get]
func (h *TestHandler) GetUserTestStats(c *gin.Context) {
userID, exists := utils.GetUserIDFromContext(c)
if !exists {
common.BadRequestResponse(c, "请先登录")
return
}
stats, err := h.testService.GetUserTestStats(utils.Int64ToString(userID))
if err != nil {
common.ErrorResponse(c, http.StatusInternalServerError, "获取测试统计失败")
return
}
common.SuccessResponse(c, stats)
}
// DeleteTestResult 删除测试结果
// @Summary 删除测试结果
// @Tags Test
// @Param id path string true "结果ID"
// @Success 200 {object} common.Response
// @Router /api/v1/tests/results/{id} [delete]
func (h *TestHandler) DeleteTestResult(c *gin.Context) {
resultID := c.Param("id")
// 验证用户权限
result, err := h.testService.GetTestResultByID(resultID)
if err != nil {
common.ErrorResponse(c, http.StatusNotFound, "测试结果不存在")
return
}
userID, _ := utils.GetUserIDFromContext(c)
if result.UserID != utils.Int64ToString(userID) {
common.ErrorResponse(c, http.StatusForbidden, "无权删除此测试结果")
return
}
if err := h.testService.DeleteTestResult(resultID); err != nil {
common.ErrorResponse(c, http.StatusInternalServerError, "删除测试结果失败")
return
}
common.SuccessResponse(c, gin.H{"message": "删除成功"})
}

View File

@@ -0,0 +1,296 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/common"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/services"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/utils"
)
// UserHandler 用户处理器
type UserHandler struct {
userService *services.UserService
validator *validator.Validate
}
// NewUserHandler 创建用户处理器实例
func NewUserHandler(userService *services.UserService) *UserHandler {
return &UserHandler{
userService: userService,
validator: validator.New(),
}
}
// UpdateUserRequest 更新用户信息请求结构
type UpdateUserRequest struct {
Username string `json:"username" validate:"omitempty,min=3,max=20"`
Email string `json:"email" validate:"omitempty,email"`
Nickname string `json:"nickname" validate:"omitempty,min=1,max=50"`
Avatar string `json:"avatar" validate:"omitempty,url"`
Timezone string `json:"timezone" validate:"omitempty"`
Language string `json:"language" validate:"omitempty"`
}
// UpdateUserPreferencesRequest 更新用户偏好设置请求结构
type UpdateUserPreferencesRequest struct {
DailyGoal int `json:"daily_goal" validate:"omitempty,min=1,max=1000"`
WeeklyGoal int `json:"weekly_goal" validate:"omitempty,min=1,max=7000"`
ReminderEnabled bool `json:"reminder_enabled"`
DifficultyLevel string `json:"difficulty_level" validate:"omitempty,oneof=beginner intermediate advanced"`
LearningMode string `json:"learning_mode" validate:"omitempty,oneof=casual intensive exam"`
}
// UserStatsResponse 用户学习统计响应结构
type UserStatsResponse struct {
TotalWords int `json:"total_words"`
LearnedWords int `json:"learned_words"`
MasteredWords int `json:"mastered_words"`
StudyDays int `json:"study_days"`
ConsecutiveDays int `json:"consecutive_days"`
TotalStudyTime int `json:"total_study_time"` // 分钟
}
// GetUserProfile 获取用户信息
func (h *UserHandler) GetUserProfile(c *gin.Context) {
userID, exists := utils.GetUserIDFromContext(c)
if !exists {
common.BadRequestResponse(c, "请先登录")
return
}
user, err := h.userService.GetUserByID(userID)
if err != nil {
if businessErr, ok := err.(*common.BusinessError); ok {
common.ErrorResponse(c, http.StatusBadRequest, businessErr.Message)
return
}
common.InternalServerErrorResponse(c, "获取用户信息失败")
return
}
// 获取用户偏好设置
preferences, err := h.userService.GetUserPreferences(userID)
if err != nil {
// 偏好设置获取失败不影响用户信息返回,记录日志即可
preferences = nil
}
// 构造响应数据
response := map[string]interface{}{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"nickname": user.Nickname,
"avatar": user.Avatar,
"timezone": user.Timezone,
"language": user.Language,
"status": user.Status,
"created_at": user.CreatedAt,
"updated_at": user.UpdatedAt,
}
if preferences != nil {
response["preferences"] = map[string]interface{}{
"daily_goal": preferences.DailyGoal,
"weekly_goal": preferences.WeeklyGoal,
"reminder_enabled": preferences.ReminderEnabled,
"difficulty_level": preferences.DifficultyLevel,
"learning_mode": preferences.LearningMode,
}
}
common.SuccessResponse(c, response)
}
// UpdateUserProfile 更新用户信息
func (h *UserHandler) UpdateUserProfile(c *gin.Context) {
userID, exists := utils.GetUserIDFromContext(c)
if !exists {
common.BadRequestResponse(c, "请先登录")
return
}
var req UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ValidationErrorResponse(c, err)
return
}
// 验证请求参数
if err := h.validator.Struct(&req); err != nil {
common.ValidationErrorResponse(c, err)
return
}
// 构造更新数据
updates := make(map[string]interface{})
if req.Username != "" {
updates["username"] = req.Username
}
if req.Email != "" {
updates["email"] = req.Email
}
if req.Nickname != "" {
updates["nickname"] = req.Nickname
}
if req.Avatar != "" {
updates["avatar"] = req.Avatar
}
if req.Timezone != "" {
updates["timezone"] = req.Timezone
}
if req.Language != "" {
updates["language"] = req.Language
}
if len(updates) == 0 {
common.BadRequestResponse(c, "没有需要更新的字段")
return
}
// 更新用户信息
user, err := h.userService.UpdateUser(userID, updates)
if err != nil {
if businessErr, ok := err.(*common.BusinessError); ok {
common.ErrorResponse(c, http.StatusBadRequest, businessErr.Message)
return
}
common.InternalServerErrorResponse(c, "更新用户信息失败")
return
}
common.SuccessResponse(c, user)
}
// UpdateUserPreferences 更新用户偏好设置
func (h *UserHandler) UpdateUserPreferences(c *gin.Context) {
userID, exists := utils.GetUserIDFromContext(c)
if !exists {
common.BadRequestResponse(c, "请先登录")
return
}
var req UpdateUserPreferencesRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ValidationErrorResponse(c, err)
return
}
// 验证请求参数
if err := h.validator.Struct(&req); err != nil {
common.ValidationErrorResponse(c, err)
return
}
// 构造更新数据
updates := make(map[string]interface{})
if req.DailyGoal > 0 {
updates["daily_goal"] = req.DailyGoal
}
if req.WeeklyGoal > 0 {
updates["weekly_goal"] = req.WeeklyGoal
}
updates["reminder_enabled"] = req.ReminderEnabled
if req.DifficultyLevel != "" {
updates["difficulty_level"] = req.DifficultyLevel
}
if req.LearningMode != "" {
updates["learning_mode"] = req.LearningMode
}
// 更新用户偏好设置
preferences, err := h.userService.UpdateUserPreferences(userID, updates)
if err != nil {
if businessErr, ok := err.(*common.BusinessError); ok {
common.ErrorResponse(c, http.StatusBadRequest, businessErr.Message)
return
}
common.InternalServerErrorResponse(c, "更新偏好设置失败")
return
}
common.SuccessResponse(c, preferences)
}
// GetUserStats 获取用户学习统计
func (h *UserHandler) GetUserStats(c *gin.Context) {
userID, exists := utils.GetUserIDFromContext(c)
if !exists {
common.BadRequestResponse(c, "请先登录")
return
}
// 获取时间范围参数
timeRange := c.DefaultQuery("time_range", "all") // all, week, month, year
// 这里需要实现具体的统计逻辑,暂时返回模拟数据
// TODO: 实现真实的统计查询使用userID和timeRange参数
_ = userID // 避免未使用变量错误
_ = timeRange // 避免未使用变量错误
stats := &UserStatsResponse{
TotalWords: 1000,
LearnedWords: 750,
MasteredWords: 500,
StudyDays: 30,
ConsecutiveDays: 7,
TotalStudyTime: 1800, // 30小时
}
common.SuccessResponse(c, stats)
}
// GetUserLearningProgress 获取用户学习进度
func (h *UserHandler) GetUserLearningProgress(c *gin.Context) {
userID, exists := utils.GetUserIDFromContext(c)
if !exists {
common.BadRequestResponse(c, "请先登录")
return
}
// 获取分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 20
}
// 获取过滤参数
masteryLevel := c.Query("mastery_level")
categoryID := c.Query("category_id")
// 调用词汇服务获取用户的学习进度
progressList, total, err := h.userService.GetUserLearningProgress(utils.Int64ToString(userID), page, limit)
if err != nil {
common.InternalServerErrorResponse(c, "获取学习进度失败")
return
}
totalPages := int64(0)
if limit > 0 {
totalPages = (total + int64(limit) - 1) / int64(limit)
}
response := map[string]interface{}{
"progress": progressList,
"pagination": map[string]interface{}{
"page": page,
"limit": limit,
"total": total,
"total_page": totalPages,
},
"filters": map[string]interface{}{
"mastery_level": masteryLevel,
"category_id": categoryID,
},
}
common.SuccessResponse(c, response)
}

View File

@@ -0,0 +1,761 @@
package handlers
import (
"net/http"
"strconv"
"time"
"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"
"github.com/google/uuid"
)
// WritingHandler 写作练习处理器
type WritingHandler struct {
writingService *services.WritingService
}
// NewWritingHandler 创建写作练习处理器实例
func NewWritingHandler(writingService *services.WritingService) *WritingHandler {
return &WritingHandler{
writingService: writingService,
}
}
// ===== 请求和响应结构体 =====
// CreateWritingPromptRequest 创建写作题目请求
type CreateWritingPromptRequest struct {
Title string `json:"title" binding:"required"`
Prompt string `json:"prompt" binding:"required"`
Instructions *string `json:"instructions"`
MinWords *int `json:"min_words"`
MaxWords *int `json:"max_words"`
TimeLimit *int `json:"time_limit"`
Level string `json:"level" binding:"required"`
Category string `json:"category"`
Tags *string `json:"tags"`
SampleAnswer *string `json:"sample_answer"`
Rubric *string `json:"rubric"`
}
// UpdateWritingPromptRequest 更新写作题目请求
type UpdateWritingPromptRequest struct {
Title *string `json:"title"`
Prompt *string `json:"prompt"`
Instructions *string `json:"instructions"`
MinWords *int `json:"min_words"`
MaxWords *int `json:"max_words"`
TimeLimit *int `json:"time_limit"`
Level *string `json:"level"`
Category *string `json:"category"`
Tags *string `json:"tags"`
SampleAnswer *string `json:"sample_answer"`
Rubric *string `json:"rubric"`
}
// CreateWritingSubmissionRequest 创建写作提交请求
type CreateWritingSubmissionRequest struct {
PromptID string `json:"prompt_id" binding:"required"`
}
// SubmitWritingRequest 提交写作请求
type SubmitWritingRequest struct {
Content string `json:"content" binding:"required"`
TimeSpent int `json:"time_spent" binding:"required"`
}
// GradeWritingRequest AI批改请求
type GradeWritingRequest struct {
Score float64 `json:"score" binding:"required,min=0,max=100"`
GrammarScore float64 `json:"grammar_score" binding:"required,min=0,max=100"`
VocabScore float64 `json:"vocab_score" binding:"required,min=0,max=100"`
CoherenceScore float64 `json:"coherence_score" binding:"required,min=0,max=100"`
Feedback string `json:"feedback" binding:"required"`
Suggestions string `json:"suggestions"`
}
// Response 通用响应结构
type Response struct {
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// ===== 写作题目管理接口 =====
// GetWritingPrompts 获取写作题目列表
// @Summary 获取写作题目列表
// @Description 获取写作题目列表,支持按难度和分类筛选
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param difficulty query string false "难度筛选"
// @Param category query string false "分类筛选"
// @Param page query int false "页码" default(1)
// @Param limit query int false "每页数量" default(10)
// @Success 200 {object} Response
// @Router /writing/prompts [get]
func (h *WritingHandler) GetWritingPrompts(c *gin.Context) {
difficulty := c.Query("difficulty")
category := c.Query("category")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
offset := (page - 1) * limit
prompts, err := h.writingService.GetWritingPrompts(difficulty, category, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取写作题目失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "获取写作题目成功",
"data": prompts,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": len(prompts),
},
})
}
// GetWritingPrompt 获取单个写作题目
// @Summary 获取写作题目详情
// @Description 根据ID获取写作题目详情
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param id path string true "题目ID"
// @Success 200 {object} Response
// @Router /writing/prompts/{id} [get]
func (h *WritingHandler) GetWritingPrompt(c *gin.Context) {
id := c.Param("id")
prompt, err := h.writingService.GetWritingPrompt(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "写作题目不存在",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "获取写作题目成功",
"data": prompt,
})
}
// CreateWritingPrompt 创建写作题目
// @Summary 创建写作题目
// @Description 创建新的写作题目
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param request body CreateWritingPromptRequest true "创建请求"
// @Success 201 {object} Response
// @Router /writing/prompts [post]
func (h *WritingHandler) CreateWritingPrompt(c *gin.Context) {
var req CreateWritingPromptRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
prompt := &models.WritingPrompt{
ID: uuid.New().String(),
Title: req.Title,
Prompt: req.Prompt,
Instructions: req.Instructions,
MinWords: req.MinWords,
MaxWords: req.MaxWords,
TimeLimit: req.TimeLimit,
Level: req.Level,
Category: req.Category,
Tags: req.Tags,
SampleAnswer: req.SampleAnswer,
Rubric: req.Rubric,
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := h.writingService.CreateWritingPrompt(prompt); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "创建写作题目失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "写作题目创建成功",
"data": prompt,
})
}
// UpdateWritingPrompt 更新写作题目
// @Summary 更新写作题目
// @Description 更新写作题目信息
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param id path string true "题目ID"
// @Param request body UpdateWritingPromptRequest true "更新请求"
// @Success 200 {object} Response
// @Router /writing/prompts/{id} [put]
func (h *WritingHandler) UpdateWritingPrompt(c *gin.Context) {
id := c.Param("id")
var req UpdateWritingPromptRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
// 检查题目是否存在
existingPrompt, err := h.writingService.GetWritingPrompt(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "写作题目不存在",
"details": err.Error(),
})
return
}
// 构建更新数据
updateData := &models.WritingPrompt{
UpdatedAt: time.Now(),
}
if req.Title != nil {
updateData.Title = *req.Title
}
if req.Prompt != nil {
updateData.Prompt = *req.Prompt
}
if req.Instructions != nil {
updateData.Instructions = req.Instructions
}
if req.MinWords != nil {
updateData.MinWords = req.MinWords
}
if req.MaxWords != nil {
updateData.MaxWords = req.MaxWords
}
if req.TimeLimit != nil {
updateData.TimeLimit = req.TimeLimit
}
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.SampleAnswer != nil {
updateData.SampleAnswer = req.SampleAnswer
}
if req.Rubric != nil {
updateData.Rubric = req.Rubric
}
if err := h.writingService.UpdateWritingPrompt(id, updateData); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "更新写作题目失败",
"details": err.Error(),
})
return
}
// 返回更新后的题目
updatedPrompt, _ := h.writingService.GetWritingPrompt(id)
c.JSON(http.StatusOK, gin.H{
"message": "写作题目更新成功",
"data": updatedPrompt,
"original": existingPrompt,
})
}
// DeleteWritingPrompt 删除写作题目
// @Summary 删除写作题目
// @Description 软删除写作题目
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param id path string true "题目ID"
// @Success 200 {object} Response
// @Router /writing/prompts/{id} [delete]
func (h *WritingHandler) DeleteWritingPrompt(c *gin.Context) {
id := c.Param("id")
// 检查题目是否存在
_, err := h.writingService.GetWritingPrompt(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "写作题目不存在",
"details": err.Error(),
})
return
}
if err := h.writingService.DeleteWritingPrompt(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "删除写作题目失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "写作题目删除成功",
})
}
// SearchWritingPrompts 搜索写作题目
// @Summary 搜索写作题目
// @Description 根据关键词搜索写作题目
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param keyword query string true "搜索关键词"
// @Param difficulty query string false "难度筛选"
// @Param category query string false "分类筛选"
// @Param page query int false "页码" default(1)
// @Param limit query int false "每页数量" default(10)
// @Success 200 {object} Response
// @Router /writing/prompts/search [get]
func (h *WritingHandler) SearchWritingPrompts(c *gin.Context) {
keyword := c.Query("keyword")
if keyword == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "搜索关键词不能为空",
})
return
}
difficulty := c.Query("difficulty")
category := c.Query("category")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
offset := (page - 1) * limit
prompts, err := h.writingService.SearchWritingPrompts(keyword, difficulty, category, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "搜索写作题目失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "搜索写作题目成功",
"data": prompts,
"search_params": gin.H{
"keyword": keyword,
"difficulty": difficulty,
"category": category,
},
"pagination": gin.H{
"page": page,
"limit": limit,
"total": len(prompts),
},
})
}
// GetRecommendedPrompts 获取推荐写作题目
// @Summary 获取推荐写作题目
// @Description 根据用户历史表现推荐合适的写作题目
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param limit query int false "推荐数量" default(5)
// @Success 200 {object} Response
// @Router /writing/prompts/recommendations [get]
func (h *WritingHandler) GetRecommendedPrompts(c *gin.Context) {
userID, ok := utils.GetUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未认证"})
return
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "5"))
prompts, err := h.writingService.GetRecommendedPrompts(utils.Int64ToString(userID), limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取推荐题目失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "获取推荐题目成功",
"data": prompts,
})
}
// ===== 写作提交管理接口 =====
// CreateWritingSubmission 创建写作提交
// @Summary 创建写作提交
// @Description 开始写作练习,创建写作提交记录
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param request body CreateWritingSubmissionRequest true "创建请求"
// @Success 201 {object} Response
// @Router /writing/submissions [post]
func (h *WritingHandler) CreateWritingSubmission(c *gin.Context) {
userID, ok := utils.GetUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未认证"})
return
}
var req CreateWritingSubmissionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
// 检查是否已有该题目的提交记录
existingSubmission, err := h.writingService.GetWritingProgress(utils.Int64ToString(userID), req.PromptID)
if err == nil && existingSubmission != nil {
c.JSON(http.StatusOK, gin.H{
"message": "写作提交记录已存在",
"data": existingSubmission,
})
return
}
submission := &models.WritingSubmission{
ID: uuid.New().String(),
UserID: utils.Int64ToString(userID),
PromptID: req.PromptID,
StartedAt: time.Now(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := h.writingService.CreateWritingSubmission(submission); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "创建写作提交失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "写作提交创建成功",
"data": submission,
})
}
// SubmitWriting 提交写作作业
// @Summary 提交写作作业
// @Description 提交完成的写作内容
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param id path string true "提交ID"
// @Param request body SubmitWritingRequest true "提交请求"
// @Success 200 {object} Response
// @Router /writing/submissions/{id}/submit [put]
func (h *WritingHandler) SubmitWriting(c *gin.Context) {
userID, ok := utils.GetUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未认证"})
return
}
submissionID := c.Param("id")
var req SubmitWritingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
// 检查提交记录是否存在且属于当前用户
submission, err := h.writingService.GetWritingSubmission(submissionID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "写作提交不存在",
"details": err.Error(),
})
return
}
if submission.UserID != utils.Int64ToString(userID) {
c.JSON(http.StatusForbidden, gin.H{
"error": "无权限访问此提交记录",
})
return
}
if submission.SubmittedAt != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "该写作已经提交,无法重复提交",
})
return
}
if err := h.writingService.SubmitWriting(submissionID, req.Content, req.TimeSpent); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "提交写作失败",
"details": err.Error(),
})
return
}
// 返回更新后的提交记录
updatedSubmission, _ := h.writingService.GetWritingSubmission(submissionID)
c.JSON(http.StatusOK, gin.H{
"message": "写作提交成功",
"data": updatedSubmission,
})
}
// GradeWriting AI批改写作
// @Summary AI批改写作
// @Description 对提交的写作进行AI批改和评分
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param id path string true "提交ID"
// @Param request body GradeWritingRequest true "批改请求"
// @Success 200 {object} Response
// @Router /writing/submissions/{id}/grade [put]
func (h *WritingHandler) GradeWriting(c *gin.Context) {
submissionID := c.Param("id")
var req GradeWritingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "请求参数错误",
"details": err.Error(),
})
return
}
// 检查提交记录是否存在
submission, err := h.writingService.GetWritingSubmission(submissionID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "写作提交不存在",
"details": err.Error(),
})
return
}
if submission.SubmittedAt == nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "该写作尚未提交,无法批改",
})
return
}
if submission.GradedAt != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "该写作已经批改,无法重复批改",
})
return
}
if err := h.writingService.GradeWriting(
submissionID,
req.Score,
req.GrammarScore,
req.VocabScore,
req.CoherenceScore,
req.Feedback,
req.Suggestions,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "批改写作失败",
"details": err.Error(),
})
return
}
// 返回更新后的提交记录
updatedSubmission, _ := h.writingService.GetWritingSubmission(submissionID)
c.JSON(http.StatusOK, gin.H{
"message": "写作批改成功",
"data": updatedSubmission,
})
}
// GetWritingSubmission 获取写作提交详情
// @Summary 获取写作提交详情
// @Description 根据ID获取写作提交详情
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param id path string true "提交ID"
// @Success 200 {object} Response
// @Router /writing/submissions/{id} [get]
func (h *WritingHandler) GetWritingSubmission(c *gin.Context) {
userID, ok := utils.GetUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未认证"})
return
}
submissionID := c.Param("id")
submission, err := h.writingService.GetWritingSubmission(submissionID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "写作提交不存在",
"details": err.Error(),
})
return
}
// 检查权限
if submission.UserID != utils.Int64ToString(userID) {
c.JSON(http.StatusForbidden, gin.H{
"error": "无权限访问此提交记录",
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "获取写作提交成功",
"data": submission,
})
}
// GetUserWritingSubmissions 获取用户写作提交列表
// @Summary 获取用户写作提交列表
// @Description 获取当前用户的写作提交列表
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param page query int false "页码" default(1)
// @Param limit query int false "每页数量" default(10)
// @Success 200 {object} Response
// @Router /writing/submissions [get]
func (h *WritingHandler) GetUserWritingSubmissions(c *gin.Context) {
userID, ok := utils.GetUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未认证"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
offset := (page - 1) * limit
submissions, err := h.writingService.GetUserWritingSubmissions(utils.Int64ToString(userID), limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取写作提交列表失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "获取写作提交列表成功",
"data": submissions,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": len(submissions),
},
})
}
// ===== 写作统计和进度接口 =====
// GetWritingStats 获取用户写作统计
// @Summary 获取用户写作统计
// @Description 获取用户写作学习统计数据
// @Tags 写作练习
// @Accept json
// @Produce json
// @Success 200 {object} Response
// @Router /writing/stats [get]
func (h *WritingHandler) GetWritingStats(c *gin.Context) {
userID, ok := utils.GetUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未认证"})
return
}
stats, err := h.writingService.GetUserWritingStats(utils.Int64ToString(userID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "获取写作统计失败",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "获取写作统计成功",
"data": stats,
})
}
// GetWritingProgress 获取写作进度
// @Summary 获取写作进度
// @Description 获取用户在特定题目上的写作进度
// @Tags 写作练习
// @Accept json
// @Produce json
// @Param prompt_id path string true "题目ID"
// @Success 200 {object} Response
// @Router /writing/progress/{prompt_id} [get]
func (h *WritingHandler) GetWritingProgress(c *gin.Context) {
userID, ok := utils.GetUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未认证"})
return
}
promptID := c.Param("prompt_id")
progress, err := h.writingService.GetWritingProgress(utils.Int64ToString(userID), promptID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "写作进度不存在",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "获取写作进度成功",
"data": progress,
})
}

61
serve/api/middleware.go Normal file
View File

@@ -0,0 +1,61 @@
package api
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// AuthMiddleware JWT认证中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取Authorization头
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Authorization header is required",
})
c.Abort()
return
}
// 检查Bearer token格式
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
if tokenString == authHeader {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid authorization header format",
})
c.Abort()
return
}
// TODO: 验证JWT token
// 这里应该验证token的有效性解析用户信息等
// 暂时跳过验证,直接放行
// 设置用户ID到上下文中示例
c.Set("user_id", "example_user_id")
c.Next()
}
}
// CORSMiddleware CORS中间件
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}

373
serve/api/router.go Normal file
View File

@@ -0,0 +1,373 @@
// api/router.go
package api
import (
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/api/handlers"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/config"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/database"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/handler"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/middleware"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/services"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
// SetupRouter 配置所有路由
func SetupRouter() *gin.Engine {
// 根据环境设置Gin模式
gin.SetMode(config.GlobalConfig.Server.Mode)
// 创建路由引擎,不使用默认中间件
router := gin.New()
// 添加自定义中间件
router.Use(middleware.RequestID())
router.Use(middleware.RequestResponseLogger())
router.Use(middleware.ErrorHandler())
router.Use(middleware.CORS())
router.Use(middleware.RateLimiter())
// 静态文件服务 - 提供上传文件的访问
router.Static("/uploads", "./uploads")
// 初始化数据库连接
db := database.GetDB()
// 初始化验证器
validate := validator.New()
// 初始化服务
userService := services.NewUserService(db)
vocabularyService := services.NewVocabularyService(db)
learningSessionService := services.NewLearningSessionService(db)
listeningService := services.NewListeningService(db)
readingService := services.NewReadingService(db)
writingService := services.NewWritingService(db)
speakingService := services.NewSpeakingService(db)
testService := services.NewTestService(db)
notificationService := services.NewNotificationService(db)
wordBookService := services.NewWordBookService(db)
studyPlanService := services.NewStudyPlanService(db)
// 初始化处理器
authHandler := handlers.NewAuthHandler(userService)
userHandler := handlers.NewUserHandler(userService)
vocabularyHandler := handler.NewVocabularyHandler(vocabularyService)
learningSessionHandler := handler.NewLearningSessionHandler(learningSessionService)
listeningHandler := handlers.NewListeningHandler(listeningService, validate)
readingHandler := handlers.NewReadingHandler(readingService)
writingHandler := handlers.NewWritingHandler(writingService)
speakingHandler := handlers.NewSpeakingHandler(speakingService)
testHandler := handlers.NewTestHandler(testService)
notificationHandler := handler.NewNotificationHandler(notificationService)
wordBookHandler := handler.NewWordBookHandler(wordBookService)
studyPlanHandler := handler.NewStudyPlanHandler(studyPlanService)
aiHandler := handler.NewAIHandler()
uploadHandler := handler.NewUploadHandler()
// 健康检查和系统信息路由
router.GET("/health", handlers.HealthCheck)
router.GET("/health/readiness", handlers.ReadinessCheck)
router.GET("/health/liveness", handlers.LivenessCheck)
router.GET("/version", handlers.GetVersion)
// 为 /hello 路径注册 HelloHandler
router.GET("/hello", handlers.HelloHandler)
// 认证相关路由(无需认证)
auth := router.Group("/api/v1/auth")
{
auth.POST("/register", authHandler.Register)
auth.POST("/login", authHandler.Login)
auth.POST("/refresh", authHandler.RefreshToken)
}
// 用户相关路由(需要认证)
user := router.Group("/api/v1/user")
user.Use(middleware.AuthMiddleware())
{
user.GET("/profile", userHandler.GetUserProfile)
user.PUT("/profile", userHandler.UpdateUserProfile)
user.GET("/stats", userHandler.GetUserStats)
user.GET("/learning-progress", userHandler.GetUserLearningProgress)
user.POST("/change-password", authHandler.ChangePassword)
}
// 词汇相关路由
vocabulary := router.Group("/api/v1/vocabulary")
vocabulary.Use(middleware.AuthMiddleware())
{
// 词汇分类
vocabulary.GET("/categories", vocabularyHandler.GetCategories)
vocabulary.POST("/categories", vocabularyHandler.CreateCategory)
vocabulary.PUT("/categories/:id", vocabularyHandler.UpdateCategory)
vocabulary.DELETE("/categories/:id", vocabularyHandler.DeleteCategory)
// 词汇管理
vocabulary.GET("/categories/:id/vocabularies", vocabularyHandler.GetVocabulariesByCategory)
vocabulary.GET("/:id", vocabularyHandler.GetVocabularyByID)
vocabulary.POST("/", vocabularyHandler.CreateVocabulary)
vocabulary.GET("/search", vocabularyHandler.SearchVocabularies)
// 用户单词学习进度
vocabulary.GET("/words/:id/progress", vocabularyHandler.GetUserWordProgress)
vocabulary.PUT("/words/:id/progress", vocabularyHandler.UpdateUserWordProgress)
// 用户词汇进度
vocabulary.GET("/progress/:id", vocabularyHandler.GetUserVocabularyProgress)
vocabulary.PUT("/progress/:id", vocabularyHandler.UpdateUserVocabularyProgress)
vocabulary.GET("/stats", vocabularyHandler.GetUserVocabularyStats)
// 词汇测试
vocabulary.POST("/tests", vocabularyHandler.CreateVocabularyTest)
vocabulary.GET("/tests/:id", vocabularyHandler.GetVocabularyTest)
vocabulary.PUT("/tests/:id/result", vocabularyHandler.UpdateVocabularyTestResult)
// 每日学习统计
vocabulary.GET("/daily", vocabularyHandler.GetDailyVocabularyStats)
// 学习相关API
vocabulary.GET("/study/today", vocabularyHandler.GetTodayStudyWords)
vocabulary.GET("/study/statistics", vocabularyHandler.GetStudyStatistics)
vocabulary.GET("/study/statistics/history", vocabularyHandler.GetStudyStatisticsHistory)
// 词汇书相关API
vocabulary.GET("/books/categories", vocabularyHandler.GetVocabularyBookCategories)
vocabulary.GET("/books/system", vocabularyHandler.GetSystemVocabularyBooks)
vocabulary.GET("/books/:id/words", vocabularyHandler.GetVocabularyBookWords)
vocabulary.GET("/books/:id/progress", vocabularyHandler.GetVocabularyBookProgress)
// 学习会话相关API
vocabulary.POST("/books/:id/learn", learningSessionHandler.StartLearning)
vocabulary.GET("/books/:id/tasks", learningSessionHandler.GetTodayTasks)
vocabulary.GET("/books/:id/statistics", learningSessionHandler.GetLearningStatistics)
vocabulary.POST("/words/:id/study", learningSessionHandler.SubmitWordStudy)
}
// 学习统计相关路由(全局统计,不限词汇书)
learning := router.Group("/api/v1/learning")
learning.Use(middleware.AuthMiddleware())
{
learning.GET("/today/statistics", learningSessionHandler.GetTodayOverallStatistics)
learning.GET("/today/review-words", learningSessionHandler.GetTodayReviewWords)
}
// 听力训练相关路由
listening := router.Group("/api/v1/listening")
listening.Use(middleware.AuthMiddleware())
{
// 听力材料管理
listening.GET("/materials", listeningHandler.GetListeningMaterials)
listening.GET("/materials/:id", listeningHandler.GetListeningMaterial)
listening.POST("/materials", listeningHandler.CreateListeningMaterial)
listening.PUT("/materials/:id", listeningHandler.UpdateListeningMaterial)
listening.DELETE("/materials/:id", listeningHandler.DeleteListeningMaterial)
listening.GET("/materials/search", listeningHandler.SearchListeningMaterials)
// 听力练习记录
listening.POST("/records", listeningHandler.CreateListeningRecord)
listening.PUT("/records/:id", listeningHandler.UpdateListeningRecord)
listening.GET("/records", listeningHandler.GetUserListeningRecords)
listening.GET("/records/:id", listeningHandler.GetListeningRecord)
// 听力学习统计和进度
listening.GET("/stats", listeningHandler.GetUserListeningStats)
listening.GET("/progress/:material_id", listeningHandler.GetListeningProgress)
}
// 阅读理解相关路由
reading := router.Group("/api/v1/reading")
reading.Use(middleware.AuthMiddleware())
{
// 阅读材料管理
reading.GET("/materials", readingHandler.GetReadingMaterials)
reading.GET("/materials/:id", readingHandler.GetReadingMaterial)
reading.POST("/materials", readingHandler.CreateReadingMaterial)
reading.PUT("/materials/:id", readingHandler.UpdateReadingMaterial)
reading.DELETE("/materials/:id", readingHandler.DeleteReadingMaterial)
reading.GET("/materials/search", readingHandler.SearchReadingMaterials)
// 阅读练习记录
reading.POST("/records", readingHandler.CreateReadingRecord)
reading.PUT("/records/:id", readingHandler.UpdateReadingRecord)
reading.GET("/records", readingHandler.GetUserReadingRecords)
reading.GET("/records/:id", readingHandler.GetReadingRecord)
// 阅读学习统计和进度
reading.GET("/stats", readingHandler.GetReadingStats)
reading.GET("/progress/:material_id", readingHandler.GetReadingProgress)
reading.GET("/recommendations", readingHandler.GetRecommendedMaterials)
}
// 写作练习相关路由
writing := router.Group("/api/v1/writing")
writing.Use(middleware.AuthMiddleware())
{
// 写作题目管理
writing.GET("/prompts", writingHandler.GetWritingPrompts)
writing.GET("/prompts/:id", writingHandler.GetWritingPrompt)
writing.POST("/prompts", writingHandler.CreateWritingPrompt)
writing.PUT("/prompts/:id", writingHandler.UpdateWritingPrompt)
writing.DELETE("/prompts/:id", writingHandler.DeleteWritingPrompt)
writing.GET("/prompts/search", writingHandler.SearchWritingPrompts)
writing.GET("/prompts/recommendations", writingHandler.GetRecommendedPrompts)
// 写作提交管理
writing.POST("/submissions", writingHandler.CreateWritingSubmission)
writing.GET("/submissions", writingHandler.GetUserWritingSubmissions)
writing.GET("/submissions/:id", writingHandler.GetWritingSubmission)
writing.PUT("/submissions/:id/submit", writingHandler.SubmitWriting)
writing.PUT("/submissions/:id/grade", writingHandler.GradeWriting)
// 写作学习统计和进度
writing.GET("/stats", writingHandler.GetWritingStats)
writing.GET("/progress/:prompt_id", writingHandler.GetWritingProgress)
}
// 口语练习相关路由
speaking := router.Group("/api/v1/speaking")
speaking.Use(middleware.AuthMiddleware())
{
// 口语场景管理
speaking.GET("/scenarios", speakingHandler.GetSpeakingScenarios)
speaking.GET("/scenarios/:id", speakingHandler.GetSpeakingScenario)
speaking.POST("/scenarios", speakingHandler.CreateSpeakingScenario)
speaking.PUT("/scenarios/:id", speakingHandler.UpdateSpeakingScenario)
speaking.DELETE("/scenarios/:id", speakingHandler.DeleteSpeakingScenario)
speaking.GET("/scenarios/search", speakingHandler.SearchSpeakingScenarios)
speaking.GET("/scenarios/recommendations", speakingHandler.GetRecommendedScenarios)
// 口语记录管理
speaking.POST("/records", speakingHandler.CreateSpeakingRecord)
speaking.GET("/records", speakingHandler.GetUserSpeakingRecords)
speaking.GET("/records/:id", speakingHandler.GetSpeakingRecord)
speaking.PUT("/records/:id/submit", speakingHandler.SubmitSpeaking)
speaking.PUT("/records/:id/grade", speakingHandler.GradeSpeaking)
// 口语学习统计和进度
speaking.GET("/stats", speakingHandler.GetSpeakingStats)
speaking.GET("/progress/:scenario_id", speakingHandler.GetSpeakingProgress)
}
// AI相关路由
ai := router.Group("/api/v1/ai")
ai.Use(middleware.AuthMiddleware())
{
// 写作批改
ai.POST("/writing/correct", aiHandler.CorrectWriting)
// 口语评估
ai.POST("/speaking/evaluate", aiHandler.EvaluateSpeaking)
// AI使用统计
ai.GET("/stats", aiHandler.GetAIUsageStats)
}
// 文件上传相关路由
upload := router.Group("/api/v1/upload")
upload.Use(middleware.AuthMiddleware())
{
// 音频文件上传
upload.POST("/audio", uploadHandler.UploadAudio)
// 图片文件上传
upload.POST("/image", uploadHandler.UploadImage)
// 文件信息查询
upload.GET("/file/:file_id", uploadHandler.GetFileInfo)
// 文件删除
upload.DELETE("/file/:file_id", uploadHandler.DeleteFile)
// 上传统计
upload.GET("/stats", uploadHandler.GetUploadStats)
}
// 综合测试相关路由
test := router.Group("/api/v1/tests")
test.Use(middleware.AuthMiddleware())
{
// 测试模板管理
test.GET("/templates", testHandler.GetTestTemplates)
test.GET("/templates/:id", testHandler.GetTestTemplateByID)
// 测试会话管理
test.POST("/sessions", testHandler.CreateTestSession)
test.GET("/sessions", testHandler.GetUserTestHistory)
test.GET("/sessions/:id", testHandler.GetTestSession)
test.PUT("/sessions/:id/start", testHandler.StartTest)
test.POST("/sessions/:id/answers", testHandler.SubmitAnswer)
test.PUT("/sessions/:id/pause", testHandler.PauseTest)
test.PUT("/sessions/:id/resume", testHandler.ResumeTest)
test.PUT("/sessions/:id/complete", testHandler.CompleteTest)
// 测试结果管理
test.GET("/results/:id", testHandler.GetTestResultByID)
test.DELETE("/results/:id", testHandler.DeleteTestResult)
// 测试统计
test.GET("/stats", testHandler.GetUserTestStats)
}
// 通知相关路由
notification := router.Group("/api/v1/notifications")
notification.Use(middleware.AuthMiddleware())
{
// 获取通知列表
notification.GET("", notificationHandler.GetNotifications)
// 获取未读通知数量
notification.GET("/unread-count", notificationHandler.GetUnreadCount)
// 标记通知为已读
notification.PUT("/:id/read", notificationHandler.MarkAsRead)
// 标记所有通知为已读
notification.PUT("/read-all", notificationHandler.MarkAllAsRead)
// 删除通知
notification.DELETE("/:id", notificationHandler.DeleteNotification)
}
// 生词本相关路由
wordBook := router.Group("/api/v1/word-book")
wordBook.Use(middleware.AuthMiddleware())
{
// 切换收藏状态
wordBook.POST("/toggle/:id", wordBookHandler.ToggleFavorite)
// 获取生词本列表
wordBook.GET("", wordBookHandler.GetFavoriteWords)
// 获取指定词汇书的生词本
wordBook.GET("/books/:id", wordBookHandler.GetFavoriteWordsByBook)
// 获取生词本统计
wordBook.GET("/stats", wordBookHandler.GetFavoriteStats)
// 批量添加到生词本
wordBook.POST("/batch", wordBookHandler.BatchAddToFavorite)
// 从生词本移除
wordBook.DELETE("/:id", wordBookHandler.RemoveFromFavorite)
}
// 学习计划相关路由
studyPlan := router.Group("/api/v1/study-plans")
studyPlan.Use(middleware.AuthMiddleware())
{
// 创建学习计划
studyPlan.POST("", studyPlanHandler.CreateStudyPlan)
// 获取学习计划列表
studyPlan.GET("", studyPlanHandler.GetUserStudyPlans)
// 获取今日计划
studyPlan.GET("/today", studyPlanHandler.GetTodayStudyPlans)
// 获取计划详情
studyPlan.GET("/:id", studyPlanHandler.GetStudyPlanByID)
// 更新计划
studyPlan.PUT("/:id", studyPlanHandler.UpdateStudyPlan)
// 删除计划
studyPlan.DELETE("/:id", studyPlanHandler.DeleteStudyPlan)
// 更新计划状态
studyPlan.PATCH("/:id/status", studyPlanHandler.UpdatePlanStatus)
// 记录学习进度
studyPlan.POST("/:id/progress", studyPlanHandler.RecordStudyProgress)
// 获取计划统计
studyPlan.GET("/:id/statistics", studyPlanHandler.GetStudyPlanStatistics)
}
return router
}