- Introduced new API endpoints for rendering textures, avatars, capes, and previews, enhancing the texture handling capabilities. - Implemented corresponding service methods in the TextureHandler to process rendering requests and return appropriate responses. - Updated the TextureRenderService interface to include methods for rendering textures, avatars, and capes, along with their respective parameters. - Enhanced error handling for invalid texture IDs and added support for different rendering types and formats. - Updated go.mod to include the webp library for image processing.
484 lines
12 KiB
Go
484 lines
12 KiB
Go
package handler
|
||
|
||
import (
|
||
"carrotskin/internal/container"
|
||
"carrotskin/internal/model"
|
||
"carrotskin/internal/service"
|
||
"carrotskin/internal/types"
|
||
"strconv"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// TextureHandler 材质处理器(依赖注入版本)
|
||
type TextureHandler struct {
|
||
container *container.Container
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// NewTextureHandler 创建TextureHandler实例
|
||
func NewTextureHandler(c *container.Container) *TextureHandler {
|
||
return &TextureHandler{
|
||
container: c,
|
||
logger: c.Logger,
|
||
}
|
||
}
|
||
|
||
// GenerateUploadURL 生成材质上传URL
|
||
func (h *TextureHandler) GenerateUploadURL(c *gin.Context) {
|
||
userID, ok := GetUserIDFromContext(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
var req types.GenerateTextureUploadURLRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
RespondBadRequest(c, "请求参数错误", err)
|
||
return
|
||
}
|
||
|
||
if h.container.Storage == nil {
|
||
RespondServerError(c, "存储服务不可用", nil)
|
||
return
|
||
}
|
||
|
||
result, err := h.container.UploadService.GenerateTextureUploadURL(
|
||
c.Request.Context(),
|
||
userID,
|
||
req.FileName,
|
||
string(req.TextureType),
|
||
)
|
||
if err != nil {
|
||
h.logger.Error("生成材质上传URL失败",
|
||
zap.Int64("user_id", userID),
|
||
zap.String("file_name", req.FileName),
|
||
zap.String("texture_type", string(req.TextureType)),
|
||
zap.Error(err),
|
||
)
|
||
RespondBadRequest(c, err.Error(), nil)
|
||
return
|
||
}
|
||
|
||
RespondSuccess(c, &types.GenerateTextureUploadURLResponse{
|
||
PostURL: result.PostURL,
|
||
FormData: result.FormData,
|
||
TextureURL: result.FileURL,
|
||
ExpiresIn: 900,
|
||
})
|
||
}
|
||
|
||
// Create 创建材质记录
|
||
func (h *TextureHandler) Create(c *gin.Context) {
|
||
userID, ok := GetUserIDFromContext(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
var req types.CreateTextureRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
RespondBadRequest(c, "请求参数错误", err)
|
||
return
|
||
}
|
||
|
||
maxTextures := h.container.UserService.GetMaxTexturesPerUser()
|
||
if err := h.container.TextureService.CheckUploadLimit(c.Request.Context(), userID, maxTextures); err != nil {
|
||
RespondBadRequest(c, err.Error(), nil)
|
||
return
|
||
}
|
||
|
||
texture, err := h.container.TextureService.Create(
|
||
c.Request.Context(),
|
||
userID,
|
||
req.Name,
|
||
req.Description,
|
||
string(req.Type),
|
||
req.URL,
|
||
req.Hash,
|
||
req.Size,
|
||
req.IsPublic,
|
||
req.IsSlim,
|
||
)
|
||
if err != nil {
|
||
h.logger.Error("创建材质失败",
|
||
zap.Int64("user_id", userID),
|
||
zap.String("name", req.Name),
|
||
zap.Error(err),
|
||
)
|
||
RespondBadRequest(c, err.Error(), nil)
|
||
return
|
||
}
|
||
|
||
RespondSuccess(c, TextureToTextureInfo(texture))
|
||
}
|
||
|
||
// Get 获取材质详情
|
||
func (h *TextureHandler) Get(c *gin.Context) {
|
||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
RespondBadRequest(c, "无效的材质ID", err)
|
||
return
|
||
}
|
||
|
||
texture, err := h.container.TextureService.GetByID(c.Request.Context(), id)
|
||
if err != nil {
|
||
RespondNotFound(c, err.Error())
|
||
return
|
||
}
|
||
|
||
RespondSuccess(c, TextureToTextureInfo(texture))
|
||
}
|
||
|
||
// Search 搜索材质
|
||
func (h *TextureHandler) Search(c *gin.Context) {
|
||
keyword := c.Query("keyword")
|
||
textureTypeStr := c.Query("type")
|
||
publicOnly := c.Query("public_only") == "true"
|
||
|
||
page := parseIntWithDefault(c.DefaultQuery("page", "1"), 1)
|
||
pageSize := parseIntWithDefault(c.DefaultQuery("page_size", "20"), 20)
|
||
|
||
var textureType model.TextureType
|
||
switch textureTypeStr {
|
||
case "SKIN":
|
||
textureType = model.TextureTypeSkin
|
||
case "CAPE":
|
||
textureType = model.TextureTypeCape
|
||
}
|
||
|
||
textures, total, err := h.container.TextureService.Search(c.Request.Context(), keyword, textureType, publicOnly, page, pageSize)
|
||
if err != nil {
|
||
h.logger.Error("搜索材质失败", zap.String("keyword", keyword), zap.Error(err))
|
||
RespondServerError(c, "搜索材质失败", err)
|
||
return
|
||
}
|
||
|
||
// 返回格式:
|
||
// {
|
||
// "code": 200,
|
||
// "message": "操作成功",
|
||
// "data": {
|
||
// "list": [...],
|
||
// "total": 1,
|
||
// "page": 1,
|
||
// "per_page": 5
|
||
// }
|
||
// }
|
||
RespondSuccess(c, gin.H{
|
||
"list": TexturesToTextureInfos(textures),
|
||
"total": total,
|
||
"page": page,
|
||
"per_page": pageSize,
|
||
})
|
||
}
|
||
|
||
// RenderTexture 渲染皮肤/披风预览
|
||
func (h *TextureHandler) RenderTexture(c *gin.Context) {
|
||
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
RespondBadRequest(c, "无效的材质ID", err)
|
||
return
|
||
}
|
||
renderType := service.RenderType(c.DefaultQuery("type", string(service.RenderTypeIsometric)))
|
||
size := parseIntWithDefault(c.DefaultQuery("size", "256"), 256)
|
||
format := service.ImageFormat(c.DefaultQuery("format", string(service.ImageFormatPNG)))
|
||
|
||
result, err := h.container.TextureRenderService.RenderTexture(c.Request.Context(), textureID, renderType, size, format)
|
||
if err != nil {
|
||
RespondBadRequest(c, err.Error(), err)
|
||
return
|
||
}
|
||
RespondSuccess(c, toRenderResponse(result))
|
||
}
|
||
|
||
// RenderAvatar 渲染头像(2D/3D)
|
||
func (h *TextureHandler) RenderAvatar(c *gin.Context) {
|
||
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
RespondBadRequest(c, "无效的材质ID", err)
|
||
return
|
||
}
|
||
mode := service.AvatarMode(c.DefaultQuery("mode", string(service.AvatarMode2D)))
|
||
size := parseIntWithDefault(c.DefaultQuery("size", "256"), 256)
|
||
format := service.ImageFormat(c.DefaultQuery("format", string(service.ImageFormatPNG)))
|
||
|
||
result, err := h.container.TextureRenderService.RenderAvatar(c.Request.Context(), textureID, size, mode, format)
|
||
if err != nil {
|
||
RespondBadRequest(c, err.Error(), err)
|
||
return
|
||
}
|
||
RespondSuccess(c, toRenderResponse(result))
|
||
}
|
||
|
||
// RenderCape 渲染披风
|
||
func (h *TextureHandler) RenderCape(c *gin.Context) {
|
||
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
RespondBadRequest(c, "无效的材质ID", err)
|
||
return
|
||
}
|
||
size := parseIntWithDefault(c.DefaultQuery("size", "256"), 256)
|
||
format := service.ImageFormat(c.DefaultQuery("format", string(service.ImageFormatPNG)))
|
||
|
||
result, err := h.container.TextureRenderService.RenderCape(c.Request.Context(), textureID, size, format)
|
||
if err != nil {
|
||
RespondBadRequest(c, err.Error(), err)
|
||
return
|
||
}
|
||
RespondSuccess(c, toRenderResponse(result))
|
||
}
|
||
|
||
// RenderPreview 自动选择预览(皮肤走等距,披风走披风渲染)
|
||
func (h *TextureHandler) RenderPreview(c *gin.Context) {
|
||
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
RespondBadRequest(c, "无效的材质ID", err)
|
||
return
|
||
}
|
||
size := parseIntWithDefault(c.DefaultQuery("size", "256"), 256)
|
||
format := service.ImageFormat(c.DefaultQuery("format", string(service.ImageFormatPNG)))
|
||
|
||
result, err := h.container.TextureRenderService.RenderPreview(c.Request.Context(), textureID, size, format)
|
||
if err != nil {
|
||
RespondBadRequest(c, err.Error(), err)
|
||
return
|
||
}
|
||
RespondSuccess(c, toRenderResponse(result))
|
||
}
|
||
|
||
// toRenderResponse 转换为API响应
|
||
func toRenderResponse(r *service.RenderResult) *types.RenderResponse {
|
||
if r == nil {
|
||
return nil
|
||
}
|
||
resp := &types.RenderResponse{
|
||
URL: r.URL,
|
||
ContentType: r.ContentType,
|
||
ETag: r.ETag,
|
||
Size: r.Size,
|
||
}
|
||
if !r.LastModified.IsZero() {
|
||
t := r.LastModified
|
||
resp.LastModified = &t
|
||
}
|
||
return resp
|
||
}
|
||
|
||
// Update 更新材质
|
||
func (h *TextureHandler) Update(c *gin.Context) {
|
||
userID, ok := GetUserIDFromContext(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
RespondBadRequest(c, "无效的材质ID", err)
|
||
return
|
||
}
|
||
|
||
var req types.UpdateTextureRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
RespondBadRequest(c, "请求参数错误", err)
|
||
return
|
||
}
|
||
|
||
texture, err := h.container.TextureService.Update(c.Request.Context(), textureID, userID, req.Name, req.Description, req.IsPublic)
|
||
if err != nil {
|
||
h.logger.Error("更新材质失败",
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("texture_id", textureID),
|
||
zap.Error(err),
|
||
)
|
||
RespondForbidden(c, err.Error())
|
||
return
|
||
}
|
||
|
||
RespondSuccess(c, TextureToTextureInfo(texture))
|
||
}
|
||
|
||
// Delete 删除材质
|
||
func (h *TextureHandler) Delete(c *gin.Context) {
|
||
userID, ok := GetUserIDFromContext(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
RespondBadRequest(c, "无效的材质ID", err)
|
||
return
|
||
}
|
||
|
||
if err := h.container.TextureService.Delete(c.Request.Context(), textureID, userID); err != nil {
|
||
h.logger.Error("删除材质失败",
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("texture_id", textureID),
|
||
zap.Error(err),
|
||
)
|
||
RespondForbidden(c, err.Error())
|
||
return
|
||
}
|
||
|
||
RespondSuccess(c, nil)
|
||
}
|
||
|
||
// ToggleFavorite 切换收藏状态
|
||
func (h *TextureHandler) ToggleFavorite(c *gin.Context) {
|
||
userID, ok := GetUserIDFromContext(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
RespondBadRequest(c, "无效的材质ID", err)
|
||
return
|
||
}
|
||
|
||
isFavorited, err := h.container.TextureService.ToggleFavorite(c.Request.Context(), userID, textureID)
|
||
if err != nil {
|
||
h.logger.Error("切换收藏状态失败",
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("texture_id", textureID),
|
||
zap.Error(err),
|
||
)
|
||
RespondBadRequest(c, err.Error(), nil)
|
||
return
|
||
}
|
||
|
||
RespondSuccess(c, map[string]bool{"is_favorited": isFavorited})
|
||
}
|
||
|
||
// GetUserTextures 获取用户上传的材质列表
|
||
func (h *TextureHandler) GetUserTextures(c *gin.Context) {
|
||
userID, ok := GetUserIDFromContext(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
page := parseIntWithDefault(c.DefaultQuery("page", "1"), 1)
|
||
pageSize := parseIntWithDefault(c.DefaultQuery("page_size", "20"), 20)
|
||
|
||
textures, total, err := h.container.TextureService.GetByUserID(c.Request.Context(), userID, page, pageSize)
|
||
if err != nil {
|
||
h.logger.Error("获取用户材质列表失败", zap.Int64("user_id", userID), zap.Error(err))
|
||
RespondServerError(c, "获取材质列表失败", err)
|
||
return
|
||
}
|
||
|
||
RespondSuccess(c, gin.H{
|
||
"list": TexturesToTextureInfos(textures),
|
||
"total": total,
|
||
"page": page,
|
||
"per_page": pageSize,
|
||
})
|
||
}
|
||
|
||
// GetUserFavorites 获取用户收藏的材质列表
|
||
func (h *TextureHandler) GetUserFavorites(c *gin.Context) {
|
||
userID, ok := GetUserIDFromContext(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
page := parseIntWithDefault(c.DefaultQuery("page", "1"), 1)
|
||
pageSize := parseIntWithDefault(c.DefaultQuery("page_size", "20"), 20)
|
||
|
||
textures, total, err := h.container.TextureService.GetUserFavorites(c.Request.Context(), userID, page, pageSize)
|
||
if err != nil {
|
||
h.logger.Error("获取用户收藏列表失败", zap.Int64("user_id", userID), zap.Error(err))
|
||
RespondServerError(c, "获取收藏列表失败", err)
|
||
return
|
||
}
|
||
|
||
RespondSuccess(c, gin.H{
|
||
"list": TexturesToTextureInfos(textures),
|
||
"total": total,
|
||
"page": page,
|
||
"per_page": pageSize,
|
||
})
|
||
}
|
||
|
||
// Upload 直接上传材质文件
|
||
func (h *TextureHandler) Upload(c *gin.Context) {
|
||
userID, ok := GetUserIDFromContext(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
// 解析multipart表单
|
||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB
|
||
RespondBadRequest(c, "解析表单失败", err)
|
||
return
|
||
}
|
||
|
||
// 获取文件
|
||
file, err := c.FormFile("file")
|
||
if err != nil {
|
||
RespondBadRequest(c, "获取文件失败", err)
|
||
return
|
||
}
|
||
|
||
// 读取文件内容
|
||
src, err := file.Open()
|
||
if err != nil {
|
||
RespondBadRequest(c, "打开文件失败", err)
|
||
return
|
||
}
|
||
defer src.Close()
|
||
|
||
fileData := make([]byte, file.Size)
|
||
if _, err := src.Read(fileData); err != nil {
|
||
RespondBadRequest(c, "读取文件失败", err)
|
||
return
|
||
}
|
||
|
||
// 获取表单字段
|
||
name := c.PostForm("name")
|
||
if name == "" {
|
||
RespondBadRequest(c, "名称不能为空", nil)
|
||
return
|
||
}
|
||
|
||
description := c.PostForm("description")
|
||
textureType := c.PostForm("type")
|
||
if textureType == "" {
|
||
textureType = "SKIN" // 默认值
|
||
}
|
||
|
||
isPublic := c.PostForm("is_public") == "true"
|
||
isSlim := c.PostForm("is_slim") == "true"
|
||
|
||
// 检查上传限制
|
||
maxTextures := h.container.UserService.GetMaxTexturesPerUser()
|
||
if err := h.container.TextureService.CheckUploadLimit(c.Request.Context(), userID, maxTextures); err != nil {
|
||
RespondBadRequest(c, err.Error(), nil)
|
||
return
|
||
}
|
||
|
||
// 调用服务上传
|
||
texture, err := h.container.TextureService.UploadTexture(
|
||
c.Request.Context(),
|
||
userID,
|
||
name,
|
||
description,
|
||
textureType,
|
||
fileData,
|
||
file.Filename,
|
||
isPublic,
|
||
isSlim,
|
||
)
|
||
if err != nil {
|
||
h.logger.Error("上传材质失败",
|
||
zap.Int64("user_id", userID),
|
||
zap.String("file_name", file.Filename),
|
||
zap.Error(err),
|
||
)
|
||
RespondBadRequest(c, err.Error(), nil)
|
||
return
|
||
}
|
||
|
||
RespondSuccess(c, TextureToTextureInfo(texture))
|
||
}
|