refactor: 重构服务层和仓库层
This commit is contained in:
227
internal/handler/customskin_handler.go
Normal file
227
internal/handler/customskin_handler.go
Normal file
@@ -0,0 +1,227 @@
|
||||
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(texture.ID)
|
||||
}()
|
||||
|
||||
// 流式传输文件内容
|
||||
c.DataFromReader(http.StatusOK, objInfo.Size, objInfo.ContentType, reader, nil)
|
||||
}
|
||||
@@ -10,30 +10,32 @@ import (
|
||||
|
||||
// Handlers 集中管理所有Handler
|
||||
type Handlers struct {
|
||||
Auth *AuthHandler
|
||||
User *UserHandler
|
||||
Texture *TextureHandler
|
||||
Profile *ProfileHandler
|
||||
Captcha *CaptchaHandler
|
||||
Yggdrasil *YggdrasilHandler
|
||||
Auth *AuthHandler
|
||||
User *UserHandler
|
||||
Texture *TextureHandler
|
||||
Profile *ProfileHandler
|
||||
Captcha *CaptchaHandler
|
||||
Yggdrasil *YggdrasilHandler
|
||||
CustomSkin *CustomSkinHandler
|
||||
}
|
||||
|
||||
// NewHandlers 创建所有Handler实例
|
||||
func NewHandlers(c *container.Container) *Handlers {
|
||||
return &Handlers{
|
||||
Auth: NewAuthHandler(c),
|
||||
User: NewUserHandler(c),
|
||||
Texture: NewTextureHandler(c),
|
||||
Profile: NewProfileHandler(c),
|
||||
Captcha: NewCaptchaHandler(c),
|
||||
Yggdrasil: NewYggdrasilHandler(c),
|
||||
Auth: NewAuthHandler(c),
|
||||
User: NewUserHandler(c),
|
||||
Texture: NewTextureHandler(c),
|
||||
Profile: NewProfileHandler(c),
|
||||
Captcha: NewCaptchaHandler(c),
|
||||
Yggdrasil: NewYggdrasilHandler(c),
|
||||
CustomSkin: NewCustomSkinHandler(c),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutesWithDI 使用依赖注入注册所有路由
|
||||
func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
|
||||
// 设置Swagger文档
|
||||
SetupSwagger(router)
|
||||
// 健康检查路由
|
||||
router.GET("/health", HealthCheck)
|
||||
|
||||
// 创建Handler实例
|
||||
h := NewHandlers(c)
|
||||
@@ -61,6 +63,9 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
|
||||
|
||||
// 系统路由
|
||||
registerSystemRoutes(v1)
|
||||
|
||||
// CustomSkinAPI 路由
|
||||
registerCustomSkinRoutes(v1, h.CustomSkin)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,3 +196,22 @@ func registerSystemRoutes(v1 *gin.RouterGroup) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// registerCustomSkinRoutes 注册CustomSkinAPI路由
|
||||
// CustomSkinAPI 协议要求根地址必须以 / 结尾
|
||||
// 路由格式:
|
||||
// - {ROOT}/{USERNAME}.json - 获取玩家信息
|
||||
// - {ROOT}/textures/{hash} - 获取资源文件
|
||||
//
|
||||
// 根路径为 /api/v1/csl/
|
||||
func registerCustomSkinRoutes(v1 *gin.RouterGroup, h *CustomSkinHandler) {
|
||||
// CustomSkinAPI 路由组
|
||||
csl := v1.Group("/csl")
|
||||
{
|
||||
// 获取玩家信息: {ROOT}/{USERNAME}.json
|
||||
csl.GET("/:username", h.GetPlayerInfo)
|
||||
|
||||
// 获取资源文件: {ROOT}/textures/{hash}
|
||||
csl.GET("/textures/:hash", h.GetTexture)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,56 +4,9 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
// @title CarrotSkin API
|
||||
// @version 1.0
|
||||
// @description CarrotSkin 是一个优秀的 Minecraft 皮肤站 API 服务
|
||||
// @description
|
||||
// @description ## 功能特性
|
||||
// @description - 用户注册/登录/管理
|
||||
// @description - 材质上传/下载/管理
|
||||
// @description - Minecraft 档案管理
|
||||
// @description - 权限控制系统
|
||||
// @description - 积分系统
|
||||
// @description
|
||||
// @description ## 认证方式
|
||||
// @description 使用 JWT Token 进行身份认证,需要在请求头中包含:
|
||||
// @description ```
|
||||
// @description Authorization: Bearer <your-jwt-token>
|
||||
// @description ```
|
||||
|
||||
// @contact.name CarrotSkin Team
|
||||
// @contact.email support@carrotskin.com
|
||||
// @license.name MIT
|
||||
// @license.url https://opensource.org/licenses/MIT
|
||||
|
||||
// @host localhost:8080
|
||||
// @BasePath /api/v1
|
||||
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description Type "Bearer" followed by a space and JWT token.
|
||||
|
||||
func SetupSwagger(router *gin.Engine) {
|
||||
// Swagger文档路由
|
||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
// 健康检查接口
|
||||
router.GET("/health", HealthCheck)
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
// @Summary 健康检查
|
||||
// @Description 检查服务是否正常运行
|
||||
// @Tags system
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "成功"
|
||||
// @Router /health [get]
|
||||
func HealthCheck(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
|
||||
Reference in New Issue
Block a user