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