- Refactored multiple service and repository methods to accept context as a parameter, enhancing consistency and enabling better control over request lifecycles. - Updated handlers to utilize context in method calls, improving error handling and performance. - Cleaned up Dockerfile by removing unnecessary whitespace.
228 lines
6.4 KiB
Go
228 lines
6.4 KiB
Go
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)
|
||
}()
|
||
|
||
// 流式传输文件内容
|
||
c.DataFromReader(http.StatusOK, objInfo.Size, objInfo.ContentType, reader, nil)
|
||
}
|