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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user