init
This commit is contained in:
357
serve/api/handlers/auth_handler.go
Normal file
357
serve/api/handlers/auth_handler.go
Normal 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": "密码修改成功"})
|
||||
}
|
||||
134
serve/api/handlers/health_handler.go
Normal file
134
serve/api/handlers/health_handler.go
Normal 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(),
|
||||
})
|
||||
}
|
||||
15
serve/api/handlers/hello_handler.go
Normal file
15
serve/api/handlers/hello_handler.go
Normal 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!",
|
||||
})
|
||||
}
|
||||
593
serve/api/handlers/listening_handler.go
Normal file
593
serve/api/handlers/listening_handler.go
Normal 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,
|
||||
})
|
||||
}
|
||||
639
serve/api/handlers/reading_handler.go
Normal file
639
serve/api/handlers/reading_handler.go
Normal 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})
|
||||
}
|
||||
449
serve/api/handlers/speaking_handler.go
Normal file
449
serve/api/handlers/speaking_handler.go
Normal 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)
|
||||
}
|
||||
419
serve/api/handlers/test_handler.go
Normal file
419
serve/api/handlers/test_handler.go
Normal 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": "删除成功"})
|
||||
}
|
||||
296
serve/api/handlers/user_handler.go
Normal file
296
serve/api/handlers/user_handler.go
Normal 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)
|
||||
}
|
||||
761
serve/api/handlers/writing_handler.go
Normal file
761
serve/api/handlers/writing_handler.go
Normal 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
61
serve/api/middleware.go
Normal 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
373
serve/api/router.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user