Files
backend/internal/handler/customskin_handler.go

228 lines
6.4 KiB
Go
Raw Normal View History

2025-12-03 10:58:39 +08:00
package handler
import (
"carrotskin/internal/container"
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// CustomSkinHandler CustomSkinAPI处理器
type CustomSkinHandler struct {
container *container.Container
logger *zap.Logger
}
// NewCustomSkinHandler 创建CustomSkinHandler实例
func NewCustomSkinHandler(c *container.Container) *CustomSkinHandler {
return &CustomSkinHandler{
container: c,
logger: c.Logger,
}
}
// CustomSkinAPIResponse CustomSkinAPI响应格式
type CustomSkinAPIResponse struct {
Username string `json:"username"`
Textures map[string]string `json:"textures,omitempty"`
Skin string `json:"skin,omitempty"`
Cape string `json:"cape,omitempty"`
Elytra string `json:"elytra,omitempty"`
}
// GetPlayerInfo 获取玩家信息
// GET {ROOT}/{USERNAME}.json
func (h *CustomSkinHandler) GetPlayerInfo(c *gin.Context) {
username := c.Param("username")
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名不能为空"})
return
}
// 移除 .json 后缀(如果存在)
username = strings.TrimSuffix(username, ".json")
// 查找Profile不区分大小写
profile, err := h.container.ProfileService.GetByProfileName(c.Request.Context(), username)
if err != nil {
h.logger.Debug("未找到玩家",
zap.String("username", username),
zap.Error(err),
)
c.JSON(http.StatusNotFound, gin.H{"error": "玩家未找到"})
return
}
// 构建响应
response := CustomSkinAPIResponse{
Username: profile.Name,
}
// Profile 已经通过 GetByProfileName 预加载了 Skin 和 Cape
// 构建材质字典
textures := make(map[string]string)
hasSkin := false
hasCape := false
hasElytra := false
// 处理皮肤
if profile.SkinID != nil && profile.Skin != nil {
skinHash := profile.Skin.Hash
hasSkin = true
if profile.Skin.IsSlim {
// 如果是slim模型优先添加到slim然后添加default
textures["slim"] = skinHash
textures["default"] = skinHash
} else {
// 如果是default模型优先添加到default然后添加slim
textures["default"] = skinHash
textures["slim"] = skinHash
}
}
// 处理披风
if profile.CapeID != nil && profile.Cape != nil {
textures["cape"] = profile.Cape.Hash
hasCape = true
}
// 处理鞘翅使用cape的hash如果存在cape
if hasCape && profile.Cape != nil {
textures["elytra"] = profile.Cape.Hash
hasElytra = true
}
// 根据材质字典决定返回格式
// 根据协议如果只有皮肤使用default模型可以使用缩略格式
// 但如果有多个不同的材质或需要指定模型,使用完整格式
if hasSkin && !hasCape && !hasElytra {
// 如果只有皮肤使用缩略格式使用default模型的hash
if defaultHash, exists := textures["default"]; exists {
response.Skin = defaultHash
} else if slimHash, exists := textures["slim"]; exists {
// 如果只有slim也使用缩略格式但协议说这会导致手臂渲染错误
response.Skin = slimHash
}
} else if len(textures) > 0 {
// 如果有多个材质或需要指定模型,使用完整格式
response.Textures = textures
}
// 如果没有材质,不设置 textures 和 skin 字段(留空)
// 设置缓存头
c.Header("Cache-Control", "public, max-age=300") // 5分钟缓存
c.Header("Content-Type", "application/json; charset=utf-8")
// 响应If-Modified-Since
if modifiedSince := c.GetHeader("If-Modified-Since"); modifiedSince != "" {
if t, err := time.Parse(http.TimeFormat, modifiedSince); err == nil {
// 如果资源未修改返回304
if profile.UpdatedAt.Before(t.Add(time.Second)) {
c.Status(http.StatusNotModified)
return
}
}
}
// 设置Last-Modified
c.Header("Last-Modified", profile.UpdatedAt.UTC().Format(http.TimeFormat))
c.JSON(http.StatusOK, response)
}
// GetTexture 获取资源文件
// GET {ROOT}/textures/{hash}
func (h *CustomSkinHandler) GetTexture(c *gin.Context) {
hash := c.Param("hash")
if hash == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "资源标识符不能为空"})
return
}
// 查找Texture
texture, err := h.container.TextureService.GetByHash(c.Request.Context(), hash)
if err != nil {
h.logger.Debug("未找到材质",
zap.String("hash", hash),
zap.Error(err),
)
c.JSON(http.StatusNotFound, gin.H{"error": "资源未找到"})
return
}
// 检查材质状态
if texture.Status != 1 {
c.JSON(http.StatusNotFound, gin.H{"error": "资源不可用"})
return
}
// 解析文件URL获取bucket和objectName
if h.container.Storage == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "存储服务不可用"})
return
}
bucket, objectName, err := h.container.Storage.ParseFileURL(texture.URL)
if err != nil {
h.logger.Error("解析文件URL失败",
zap.String("url", texture.URL),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "解析文件URL失败"})
return
}
// 获取文件对象
ctx := c.Request.Context()
reader, objInfo, err := h.container.Storage.GetObject(ctx, bucket, objectName)
if err != nil {
h.logger.Error("获取文件失败",
zap.String("bucket", bucket),
zap.String("objectName", objectName),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取文件失败"})
return
}
defer reader.Close()
// 设置HTTP头
c.Header("Content-Type", objInfo.ContentType)
c.Header("Content-Length", fmt.Sprintf("%d", objInfo.Size))
c.Header("Last-Modified", objInfo.LastModified.UTC().Format(http.TimeFormat))
c.Header("ETag", objInfo.ETag)
c.Header("Cache-Control", "public, max-age=86400") // 24小时缓存
// 响应If-Modified-Since
if modifiedSince := c.GetHeader("If-Modified-Since"); modifiedSince != "" {
if t, err := time.Parse(http.TimeFormat, modifiedSince); err == nil {
// 如果资源未修改返回304
if objInfo.LastModified.Before(t.Add(time.Second)) {
c.Status(http.StatusNotModified)
return
}
}
}
// 响应If-None-Match (ETag)
if noneMatch := c.GetHeader("If-None-Match"); noneMatch != "" {
if noneMatch == objInfo.ETag || noneMatch == fmt.Sprintf(`"%s"`, objInfo.ETag) {
c.Status(http.StatusNotModified)
return
}
}
// 增加下载计数(异步)
go func() {
_ = h.container.TextureRepo.IncrementDownloadCount(ctx, texture.ID)
2025-12-03 10:58:39 +08:00
}()
// 流式传输文件内容
c.DataFromReader(http.StatusOK, objInfo.Size, objInfo.ContentType, reader, nil)
}