package handler import ( "carrotskin/internal/model" "carrotskin/internal/service" "carrotskin/internal/types" "carrotskin/pkg/config" "carrotskin/pkg/database" "carrotskin/pkg/logger" "carrotskin/pkg/storage" "net/http" "strconv" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // GenerateTextureUploadURL 生成材质上传URL // @Summary 生成材质上传URL // @Description 生成预签名URL用于上传材质文件 // @Tags texture // @Accept json // @Produce json // @Security BearerAuth // @Param request body types.GenerateTextureUploadURLRequest true "上传URL请求" // @Success 200 {object} model.Response "生成成功" // @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Router /api/v1/texture/upload-url [post] func GenerateTextureUploadURL(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, model.NewErrorResponse( model.CodeUnauthorized, model.MsgUnauthorized, nil, )) return } var req types.GenerateTextureUploadURLRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, "请求参数错误", err, )) return } // 调用UploadService生成预签名URL storageClient := storage.MustGetClient() cfg := *config.MustGetRustFSConfig() result, err := service.GenerateTextureUploadURL( c.Request.Context(), storageClient, cfg, userID.(int64), req.FileName, string(req.TextureType), ) if err != nil { logger.MustGetLogger().Error("生成材质上传URL失败", zap.Int64("user_id", userID.(int64)), zap.String("file_name", req.FileName), zap.String("texture_type", string(req.TextureType)), zap.Error(err), ) c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, err.Error(), nil, )) return } // 返回响应 c.JSON(http.StatusOK, model.NewSuccessResponse(&types.GenerateTextureUploadURLResponse{ PostURL: result.PostURL, FormData: result.FormData, TextureURL: result.FileURL, ExpiresIn: 900, // 15分钟 = 900秒 })) } // CreateTexture 创建材质记录 // @Summary 创建材质记录 // @Description 文件上传完成后,创建材质记录到数据库 // @Tags texture // @Accept json // @Produce json // @Security BearerAuth // @Param request body types.CreateTextureRequest true "创建材质请求" // @Success 200 {object} model.Response "创建成功" // @Failure 400 {object} model.ErrorResponse "请求参数错误" // @Router /api/v1/texture [post] func CreateTexture(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, model.NewErrorResponse( model.CodeUnauthorized, model.MsgUnauthorized, nil, )) return } var req types.CreateTextureRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, "请求参数错误", err, )) return } // TODO: 从配置或数据库读取限制 maxTextures := 100 if err := service.CheckTextureUploadLimit(database.MustGetDB(), userID.(int64), maxTextures); err != nil { c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, err.Error(), nil, )) return } // 创建材质 texture, err := service.CreateTexture(database.MustGetDB(), userID.(int64), req.Name, req.Description, string(req.Type), req.URL, req.Hash, req.Size, req.IsPublic, req.IsSlim, ) if err != nil { logger.MustGetLogger().Error("创建材质失败", zap.Int64("user_id", userID.(int64)), zap.String("name", req.Name), zap.Error(err), ) c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, err.Error(), nil, )) return } // 返回响应 c.JSON(http.StatusOK, model.NewSuccessResponse(&types.TextureInfo{ ID: texture.ID, UploaderID: texture.UploaderID, Name: texture.Name, Description: texture.Description, Type: types.TextureType(texture.Type), URL: texture.URL, Hash: texture.Hash, Size: texture.Size, IsPublic: texture.IsPublic, DownloadCount: texture.DownloadCount, FavoriteCount: texture.FavoriteCount, IsSlim: texture.IsSlim, Status: texture.Status, CreatedAt: texture.CreatedAt, UpdatedAt: texture.UpdatedAt, })) } // GetTexture 获取材质详情 // @Summary 获取材质详情 // @Description 根据ID获取材质详细信息 // @Tags texture // @Accept json // @Produce json // @Param id path int true "材质ID" // @Success 200 {object} model.Response "获取成功" // @Failure 404 {object} model.ErrorResponse "材质不存在" // @Router /api/v1/texture/{id} [get] func GetTexture(c *gin.Context) { idStr := c.Param("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, "无效的材质ID", err, )) return } texture, err := service.GetTextureByID(database.MustGetDB(), id) if err != nil { c.JSON(http.StatusNotFound, model.NewErrorResponse( model.CodeNotFound, err.Error(), nil, )) return } c.JSON(http.StatusOK, model.NewSuccessResponse(&types.TextureInfo{ ID: texture.ID, UploaderID: texture.UploaderID, Name: texture.Name, Description: texture.Description, Type: types.TextureType(texture.Type), URL: texture.URL, Hash: texture.Hash, Size: texture.Size, IsPublic: texture.IsPublic, DownloadCount: texture.DownloadCount, FavoriteCount: texture.FavoriteCount, IsSlim: texture.IsSlim, Status: texture.Status, CreatedAt: texture.CreatedAt, UpdatedAt: texture.UpdatedAt, })) } // SearchTextures 搜索材质 // @Summary 搜索材质 // @Description 根据关键词和类型搜索材质 // @Tags texture // @Accept json // @Produce json // @Param keyword query string false "关键词" // @Param type query string false "材质类型(SKIN/CAPE)" // @Param public_only query bool false "只看公开材质" // @Param page query int false "页码" default(1) // @Param page_size query int false "每页数量" default(20) // @Success 200 {object} model.PaginationResponse "搜索成功" // @Router /api/v1/texture [get] func SearchTextures(c *gin.Context) { keyword := c.Query("keyword") textureTypeStr := c.Query("type") publicOnly := c.Query("public_only") == "true" page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) var textureType model.TextureType switch textureTypeStr { case "SKIN": textureType = model.TextureTypeSkin case "CAPE": textureType = model.TextureTypeCape } textures, total, err := service.SearchTextures(database.MustGetDB(), keyword, textureType, publicOnly, page, pageSize) if err != nil { logger.MustGetLogger().Error("搜索材质失败", zap.String("keyword", keyword), zap.Error(err), ) c.JSON(http.StatusInternalServerError, model.NewErrorResponse( model.CodeServerError, "搜索材质失败", err, )) return } // 转换为TextureInfo textureInfos := make([]*types.TextureInfo, len(textures)) for i, texture := range textures { textureInfos[i] = &types.TextureInfo{ ID: texture.ID, UploaderID: texture.UploaderID, Name: texture.Name, Description: texture.Description, Type: types.TextureType(texture.Type), URL: texture.URL, Hash: texture.Hash, Size: texture.Size, IsPublic: texture.IsPublic, DownloadCount: texture.DownloadCount, FavoriteCount: texture.FavoriteCount, IsSlim: texture.IsSlim, Status: texture.Status, CreatedAt: texture.CreatedAt, UpdatedAt: texture.UpdatedAt, } } c.JSON(http.StatusOK, model.NewPaginationResponse(textureInfos, total, page, pageSize)) } // UpdateTexture 更新材质 // @Summary 更新材质 // @Description 更新材质信息(仅上传者可操作) // @Tags texture // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "材质ID" // @Param request body types.UpdateTextureRequest true "更新材质请求" // @Success 200 {object} model.Response "更新成功" // @Failure 403 {object} model.ErrorResponse "无权操作" // @Router /api/v1/texture/{id} [put] func UpdateTexture(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, model.NewErrorResponse( model.CodeUnauthorized, model.MsgUnauthorized, nil, )) return } idStr := c.Param("id") textureID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, "无效的材质ID", err, )) return } var req types.UpdateTextureRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, "请求参数错误", err, )) return } texture, err := service.UpdateTexture(database.MustGetDB(), textureID, userID.(int64), req.Name, req.Description, req.IsPublic) if err != nil { logger.MustGetLogger().Error("更新材质失败", zap.Int64("user_id", userID.(int64)), zap.Int64("texture_id", textureID), zap.Error(err), ) c.JSON(http.StatusForbidden, model.NewErrorResponse( model.CodeForbidden, err.Error(), nil, )) return } c.JSON(http.StatusOK, model.NewSuccessResponse(&types.TextureInfo{ ID: texture.ID, UploaderID: texture.UploaderID, Name: texture.Name, Description: texture.Description, Type: types.TextureType(texture.Type), URL: texture.URL, Hash: texture.Hash, Size: texture.Size, IsPublic: texture.IsPublic, DownloadCount: texture.DownloadCount, FavoriteCount: texture.FavoriteCount, IsSlim: texture.IsSlim, Status: texture.Status, CreatedAt: texture.CreatedAt, UpdatedAt: texture.UpdatedAt, })) } // DeleteTexture 删除材质 // @Summary 删除材质 // @Description 删除材质(软删除,仅上传者可操作) // @Tags texture // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "材质ID" // @Success 200 {object} model.Response "删除成功" // @Failure 403 {object} model.ErrorResponse "无权操作" // @Router /api/v1/texture/{id} [delete] func DeleteTexture(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, model.NewErrorResponse( model.CodeUnauthorized, model.MsgUnauthorized, nil, )) return } idStr := c.Param("id") textureID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, "无效的材质ID", err, )) return } if err := service.DeleteTexture(database.MustGetDB(), textureID, userID.(int64)); err != nil { logger.MustGetLogger().Error("删除材质失败", zap.Int64("user_id", userID.(int64)), zap.Int64("texture_id", textureID), zap.Error(err), ) c.JSON(http.StatusForbidden, model.NewErrorResponse( model.CodeForbidden, err.Error(), nil, )) return } c.JSON(http.StatusOK, model.NewSuccessResponse(nil)) } // ToggleFavorite 切换收藏状态 // @Summary 切换收藏状态 // @Description 收藏或取消收藏材质 // @Tags texture // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "材质ID" // @Success 200 {object} model.Response "切换成功" // @Router /api/v1/texture/{id}/favorite [post] func ToggleFavorite(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, model.NewErrorResponse( model.CodeUnauthorized, model.MsgUnauthorized, nil, )) return } idStr := c.Param("id") textureID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, "无效的材质ID", err, )) return } isFavorited, err := service.ToggleTextureFavorite(database.MustGetDB(), userID.(int64), textureID) if err != nil { logger.MustGetLogger().Error("切换收藏状态失败", zap.Int64("user_id", userID.(int64)), zap.Int64("texture_id", textureID), zap.Error(err), ) c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, err.Error(), nil, )) return } c.JSON(http.StatusOK, model.NewSuccessResponse(map[string]bool{ "is_favorited": isFavorited, })) } // GetUserTextures 获取用户上传的材质列表 // @Summary 获取用户上传的材质列表 // @Description 获取当前用户上传的所有材质 // @Tags texture // @Accept json // @Produce json // @Security BearerAuth // @Param page query int false "页码" default(1) // @Param page_size query int false "每页数量" default(20) // @Success 200 {object} model.PaginationResponse "获取成功" // @Router /api/v1/texture/my [get] func GetUserTextures(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, model.NewErrorResponse( model.CodeUnauthorized, model.MsgUnauthorized, nil, )) return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) textures, total, err := service.GetUserTextures(database.MustGetDB(), userID.(int64), page, pageSize) if err != nil { logger.MustGetLogger().Error("获取用户材质列表失败", zap.Int64("user_id", userID.(int64)), zap.Error(err), ) c.JSON(http.StatusInternalServerError, model.NewErrorResponse( model.CodeServerError, "获取材质列表失败", err, )) return } // 转换为TextureInfo textureInfos := make([]*types.TextureInfo, len(textures)) for i, texture := range textures { textureInfos[i] = &types.TextureInfo{ ID: texture.ID, UploaderID: texture.UploaderID, Name: texture.Name, Description: texture.Description, Type: types.TextureType(texture.Type), URL: texture.URL, Hash: texture.Hash, Size: texture.Size, IsPublic: texture.IsPublic, DownloadCount: texture.DownloadCount, FavoriteCount: texture.FavoriteCount, IsSlim: texture.IsSlim, Status: texture.Status, CreatedAt: texture.CreatedAt, UpdatedAt: texture.UpdatedAt, } } c.JSON(http.StatusOK, model.NewPaginationResponse(textureInfos, total, page, pageSize)) } // GetUserFavorites 获取用户收藏的材质列表 // @Summary 获取用户收藏的材质列表 // @Description 获取当前用户收藏的所有材质 // @Tags texture // @Accept json // @Produce json // @Security BearerAuth // @Param page query int false "页码" default(1) // @Param page_size query int false "每页数量" default(20) // @Success 200 {object} model.PaginationResponse "获取成功" // @Router /api/v1/texture/favorites [get] func GetUserFavorites(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, model.NewErrorResponse( model.CodeUnauthorized, model.MsgUnauthorized, nil, )) return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) textures, total, err := service.GetUserTextureFavorites(database.MustGetDB(), userID.(int64), page, pageSize) if err != nil { logger.MustGetLogger().Error("获取用户收藏列表失败", zap.Int64("user_id", userID.(int64)), zap.Error(err), ) c.JSON(http.StatusInternalServerError, model.NewErrorResponse( model.CodeServerError, "获取收藏列表失败", err, )) return } // 转换为TextureInfo textureInfos := make([]*types.TextureInfo, len(textures)) for i, texture := range textures { textureInfos[i] = &types.TextureInfo{ ID: texture.ID, UploaderID: texture.UploaderID, Name: texture.Name, Description: texture.Description, Type: types.TextureType(texture.Type), URL: texture.URL, Hash: texture.Hash, Size: texture.Size, IsPublic: texture.IsPublic, DownloadCount: texture.DownloadCount, FavoriteCount: texture.FavoriteCount, IsSlim: texture.IsSlim, Status: texture.Status, CreatedAt: texture.CreatedAt, UpdatedAt: texture.UpdatedAt, } } c.JSON(http.StatusOK, model.NewPaginationResponse(textureInfos, total, page, pageSize)) }