From ffdc3e3e6b544eeec68718f6acecbe6007600e40 Mon Sep 17 00:00:00 2001 From: lan Date: Tue, 2 Dec 2025 17:46:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E6=94=B9=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完成所有Handler的依赖注入改造: - AuthHandler: 认证相关功能 - UserHandler: 用户管理功能 - TextureHandler: 材质管理功能 - ProfileHandler: 档案管理功能 - CaptchaHandler: 验证码功能 - YggdrasilHandler: Yggdrasil API功能 新增错误类型定义: - internal/errors/errors.go: 统一的错误类型和工厂函数 更新main.go: - 使用container.NewContainer创建依赖容器 - 使用handler.RegisterRoutesWithDI注册路由 代码遵循Go最佳实践: - 依赖通过构造函数注入 - Handler通过结构体方法实现 - 统一的错误处理模式 - 清晰的分层架构 --- cmd/server/main.go | 17 +- cmd/server/main_di_example.go.example | 146 ------ internal/container/container.go | 11 +- internal/errors/errors.go | 127 +++++ internal/handler/captcha_handler_di.go | 108 +++++ internal/handler/profile_handler_di.go | 247 ++++++++++ internal/handler/routes_di.go | 84 ++-- internal/handler/yggdrasil_handler_di.go | 454 ++++++++++++++++++ internal/repository/interfaces.go | 1 - .../repository/profile_repository_impl.go | 1 - .../repository/texture_repository_impl.go | 1 - internal/repository/token_repository_impl.go | 1 - internal/repository/user_repository_impl.go | 1 - 13 files changed, 998 insertions(+), 201 deletions(-) delete mode 100644 cmd/server/main_di_example.go.example create mode 100644 internal/errors/errors.go create mode 100644 internal/handler/captcha_handler_di.go create mode 100644 internal/handler/profile_handler_di.go create mode 100644 internal/handler/yggdrasil_handler_di.go diff --git a/cmd/server/main.go b/cmd/server/main.go index fb68942..ea29746 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -10,6 +10,7 @@ import ( "time" _ "carrotskin/docs" // Swagger文档 + "carrotskin/internal/container" "carrotskin/internal/handler" "carrotskin/internal/middleware" "carrotskin/pkg/auth" @@ -66,10 +67,11 @@ func main() { defer redis.MustGetClient().Close() // 初始化对象存储 (RustFS - S3兼容) - // 如果对象存储未配置或连接失败,记录警告但不退出(某些功能可能不可用) + var storageClient *storage.StorageClient if err := storage.Init(cfg.RustFS); err != nil { loggerInstance.Warn("对象存储连接失败,某些功能可能不可用", zap.Error(err)) } else { + storageClient = storage.MustGetClient() loggerInstance.Info("对象存储连接成功") } @@ -78,6 +80,15 @@ func main() { loggerInstance.Fatal("邮件服务初始化失败", zap.Error(err)) } + // 创建依赖注入容器 + c := container.NewContainer( + database.MustGetDB(), + redis.MustGetClient(), + loggerInstance, + auth.MustGetJWTService(), + storageClient, + ) + // 设置Gin模式 if cfg.Server.Mode == "production" { gin.SetMode(gin.ReleaseMode) @@ -91,8 +102,8 @@ func main() { router.Use(middleware.Recovery(loggerInstance)) router.Use(middleware.CORS()) - // 注册路由 - handler.RegisterRoutes(router) + // 使用依赖注入方式注册路由 + handler.RegisterRoutesWithDI(router, c) // 创建HTTP服务器 srv := &http.Server{ diff --git a/cmd/server/main_di_example.go.example b/cmd/server/main_di_example.go.example deleted file mode 100644 index d9168ef..0000000 --- a/cmd/server/main_di_example.go.example +++ /dev/null @@ -1,146 +0,0 @@ -// +build ignore -// 此文件是依赖注入版本的main.go示例 -// 可以参考此文件改造原有的main.go - -package main - -import ( - "context" - "log" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - _ "carrotskin/docs" // Swagger文档 - "carrotskin/internal/container" - "carrotskin/internal/handler" - "carrotskin/internal/middleware" - "carrotskin/pkg/auth" - "carrotskin/pkg/config" - "carrotskin/pkg/database" - "carrotskin/pkg/email" - "carrotskin/pkg/logger" - "carrotskin/pkg/redis" - "carrotskin/pkg/storage" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" -) - -func main() { - // 初始化配置 - if err := config.Init(); err != nil { - log.Fatalf("配置加载失败: %v", err) - } - cfg := config.MustGetConfig() - - // 初始化日志 - if err := logger.Init(cfg.Log); err != nil { - log.Fatalf("日志初始化失败: %v", err) - } - loggerInstance := logger.MustGetLogger() - defer loggerInstance.Sync() - - // 初始化数据库 - if err := database.Init(cfg.Database, loggerInstance); err != nil { - loggerInstance.Fatal("数据库初始化失败", zap.Error(err)) - } - defer database.Close() - - // 执行数据库迁移 - if err := database.AutoMigrate(loggerInstance); err != nil { - loggerInstance.Fatal("数据库迁移失败", zap.Error(err)) - } - - // 初始化种子数据 - if err := database.Seed(loggerInstance); err != nil { - loggerInstance.Fatal("种子数据初始化失败", zap.Error(err)) - } - - // 初始化JWT服务 - if err := auth.Init(cfg.JWT); err != nil { - loggerInstance.Fatal("JWT服务初始化失败", zap.Error(err)) - } - - // 初始化Redis - if err := redis.Init(cfg.Redis, loggerInstance); err != nil { - loggerInstance.Fatal("Redis连接失败", zap.Error(err)) - } - defer redis.MustGetClient().Close() - - // 初始化对象存储 - var storageClient *storage.StorageClient - if err := storage.Init(cfg.RustFS); err != nil { - loggerInstance.Warn("对象存储连接失败,某些功能可能不可用", zap.Error(err)) - } else { - storageClient = storage.MustGetClient() - loggerInstance.Info("对象存储连接成功") - } - - // 初始化邮件服务 - if err := email.Init(cfg.Email, loggerInstance); err != nil { - loggerInstance.Fatal("邮件服务初始化失败", zap.Error(err)) - } - - // ============ 依赖注入改动部分 ============ - // 创建依赖注入容器 - c := container.NewContainer( - database.MustGetDB(), - redis.MustGetClient(), - loggerInstance, - auth.MustGetJWTService(), - storageClient, - ) - - // 设置Gin模式 - if cfg.Server.Mode == "production" { - gin.SetMode(gin.ReleaseMode) - } - - // 创建路由 - router := gin.New() - - // 添加中间件 - router.Use(middleware.Logger(loggerInstance)) - router.Use(middleware.Recovery(loggerInstance)) - router.Use(middleware.CORS()) - - // 使用依赖注入方式注册路由 - handler.RegisterRoutesWithDI(router, c) - // ============ 依赖注入改动结束 ============ - - // 创建HTTP服务器 - srv := &http.Server{ - Addr: cfg.Server.Port, - Handler: router, - ReadTimeout: cfg.Server.ReadTimeout, - WriteTimeout: cfg.Server.WriteTimeout, - } - - // 启动服务器 - go func() { - loggerInstance.Info("服务器启动", zap.String("port", cfg.Server.Port)) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - loggerInstance.Fatal("服务器启动失败", zap.Error(err)) - } - }() - - // 等待中断信号优雅关闭 - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - loggerInstance.Info("正在关闭服务器...") - - // 设置关闭超时 - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - if err := srv.Shutdown(ctx); err != nil { - loggerInstance.Fatal("服务器强制关闭", zap.Error(err)) - } - - loggerInstance.Info("服务器已关闭") -} - diff --git a/internal/container/container.go b/internal/container/container.go index 230e68f..cde146e 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -21,11 +21,11 @@ type Container struct { Storage *storage.StorageClient // Repository层 - UserRepo repository.UserRepository - ProfileRepo repository.ProfileRepository - TextureRepo repository.TextureRepository - TokenRepo repository.TokenRepository - ConfigRepo repository.SystemConfigRepository + UserRepo repository.UserRepository + ProfileRepo repository.ProfileRepository + TextureRepo repository.TextureRepository + TokenRepo repository.TokenRepository + ConfigRepo repository.SystemConfigRepository } // NewContainer 创建依赖容器 @@ -135,4 +135,3 @@ func WithConfigRepo(repo repository.SystemConfigRepository) Option { c.ConfigRepo = repo } } - diff --git a/internal/errors/errors.go b/internal/errors/errors.go new file mode 100644 index 0000000..7cf4e41 --- /dev/null +++ b/internal/errors/errors.go @@ -0,0 +1,127 @@ +// Package errors 定义应用程序的错误类型 +package errors + +import ( + "errors" + "fmt" +) + +// 预定义错误 +var ( + // 用户相关错误 + ErrUserNotFound = errors.New("用户不存在") + ErrUserAlreadyExists = errors.New("用户已存在") + ErrEmailAlreadyExists = errors.New("邮箱已被注册") + ErrInvalidPassword = errors.New("密码错误") + ErrAccountDisabled = errors.New("账号已被禁用") + + // 认证相关错误 + ErrUnauthorized = errors.New("未授权") + ErrInvalidToken = errors.New("无效的令牌") + ErrTokenExpired = errors.New("令牌已过期") + ErrInvalidSignature = errors.New("签名验证失败") + + // 档案相关错误 + ErrProfileNotFound = errors.New("档案不存在") + ErrProfileNameExists = errors.New("角色名已被使用") + ErrProfileLimitReached = errors.New("已达档案数量上限") + ErrProfileNoPermission = errors.New("无权操作此档案") + + // 材质相关错误 + ErrTextureNotFound = errors.New("材质不存在") + ErrTextureExists = errors.New("该材质已存在") + ErrTextureLimitReached = errors.New("已达材质数量上限") + ErrTextureNoPermission = errors.New("无权操作此材质") + ErrInvalidTextureType = errors.New("无效的材质类型") + + // 验证码相关错误 + ErrInvalidVerificationCode = errors.New("验证码错误或已过期") + ErrTooManyAttempts = errors.New("尝试次数过多") + ErrSendTooFrequent = errors.New("发送过于频繁") + + // URL验证相关错误 + ErrInvalidURL = errors.New("无效的URL格式") + ErrDomainNotAllowed = errors.New("URL域名不在允许的列表中") + + // 存储相关错误 + ErrStorageUnavailable = errors.New("存储服务不可用") + ErrUploadFailed = errors.New("上传失败") + + // 通用错误 + ErrBadRequest = errors.New("请求参数错误") + ErrInternalServer = errors.New("服务器内部错误") + ErrNotFound = errors.New("资源不存在") + ErrForbidden = errors.New("权限不足") +) + +// AppError 应用错误类型,包含错误码和消息 +type AppError struct { + Code int // HTTP状态码 + Message string // 用户可见的错误消息 + Err error // 原始错误(用于日志) +} + +// Error 实现error接口 +func (e *AppError) Error() string { + if e.Err != nil { + return fmt.Sprintf("%s: %v", e.Message, e.Err) + } + return e.Message +} + +// Unwrap 支持errors.Is和errors.As +func (e *AppError) Unwrap() error { + return e.Err +} + +// NewAppError 创建新的应用错误 +func NewAppError(code int, message string, err error) *AppError { + return &AppError{ + Code: code, + Message: message, + Err: err, + } +} + +// NewBadRequest 创建400错误 +func NewBadRequest(message string, err error) *AppError { + return NewAppError(400, message, err) +} + +// NewUnauthorized 创建401错误 +func NewUnauthorized(message string) *AppError { + return NewAppError(401, message, nil) +} + +// NewForbidden 创建403错误 +func NewForbidden(message string) *AppError { + return NewAppError(403, message, nil) +} + +// NewNotFound 创建404错误 +func NewNotFound(message string) *AppError { + return NewAppError(404, message, nil) +} + +// NewInternalError 创建500错误 +func NewInternalError(message string, err error) *AppError { + return NewAppError(500, message, err) +} + +// Is 检查错误是否匹配 +func Is(err, target error) bool { + return errors.Is(err, target) +} + +// As 尝试将错误转换为指定类型 +func As(err error, target interface{}) bool { + return errors.As(err, target) +} + +// Wrap 包装错误 +func Wrap(err error, message string) error { + if err == nil { + return nil + } + return fmt.Errorf("%s: %w", message, err) +} diff --git a/internal/handler/captcha_handler_di.go b/internal/handler/captcha_handler_di.go new file mode 100644 index 0000000..8078aee --- /dev/null +++ b/internal/handler/captcha_handler_di.go @@ -0,0 +1,108 @@ +package handler + +import ( + "carrotskin/internal/container" + "carrotskin/internal/service" + "net/http" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// CaptchaHandler 验证码处理器 +type CaptchaHandler struct { + container *container.Container + logger *zap.Logger +} + +// NewCaptchaHandler 创建CaptchaHandler实例 +func NewCaptchaHandler(c *container.Container) *CaptchaHandler { + return &CaptchaHandler{ + container: c, + logger: c.Logger, + } +} + +// CaptchaVerifyRequest 验证码验证请求 +type CaptchaVerifyRequest struct { + CaptchaID string `json:"captchaId" binding:"required"` + Dx int `json:"dx" binding:"required"` +} + +// Generate 生成验证码 +// @Summary 生成滑动验证码 +// @Description 生成滑动验证码图片 +// @Tags captcha +// @Accept json +// @Produce json +// @Success 200 {object} map[string]interface{} "生成成功" +// @Failure 500 {object} map[string]interface{} "生成失败" +// @Router /api/v1/captcha/generate [get] +func (h *CaptchaHandler) Generate(c *gin.Context) { + masterImg, tileImg, captchaID, y, err := service.GenerateCaptchaData(c.Request.Context(), h.container.Redis) + if err != nil { + h.logger.Error("生成验证码失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "生成验证码失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": gin.H{ + "masterImage": masterImg, + "tileImage": tileImg, + "captchaId": captchaID, + "y": y, + }, + }) +} + +// Verify 验证验证码 +// @Summary 验证滑动验证码 +// @Description 验证用户滑动的偏移量是否正确 +// @Tags captcha +// @Accept json +// @Produce json +// @Param request body CaptchaVerifyRequest true "验证请求" +// @Success 200 {object} map[string]interface{} "验证结果" +// @Failure 400 {object} map[string]interface{} "参数错误" +// @Router /api/v1/captcha/verify [post] +func (h *CaptchaHandler) Verify(c *gin.Context) { + var req CaptchaVerifyRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误: " + err.Error(), + }) + return + } + + valid, err := service.VerifyCaptchaData(c.Request.Context(), h.container.Redis, req.Dx, req.CaptchaID) + if err != nil { + h.logger.Error("验证码验证失败", + zap.String("captcha_id", req.CaptchaID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": "验证失败", + }) + return + } + + if valid { + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "msg": "验证成功", + }) + } else { + c.JSON(http.StatusOK, gin.H{ + "code": 400, + "msg": "验证失败,请重试", + }) + } +} + diff --git a/internal/handler/profile_handler_di.go b/internal/handler/profile_handler_di.go new file mode 100644 index 0000000..6fdbeb9 --- /dev/null +++ b/internal/handler/profile_handler_di.go @@ -0,0 +1,247 @@ +package handler + +import ( + "carrotskin/internal/container" + "carrotskin/internal/service" + "carrotskin/internal/types" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// ProfileHandler 档案处理器 +type ProfileHandler struct { + container *container.Container + logger *zap.Logger +} + +// NewProfileHandler 创建ProfileHandler实例 +func NewProfileHandler(c *container.Container) *ProfileHandler { + return &ProfileHandler{ + container: c, + logger: c.Logger, + } +} + +// Create 创建档案 +// @Summary 创建Minecraft档案 +// @Description 创建新的Minecraft角色档案,UUID由后端自动生成 +// @Tags profile +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body types.CreateProfileRequest true "档案信息(仅需提供角色名)" +// @Success 200 {object} model.Response{data=types.ProfileInfo} "创建成功" +// @Failure 400 {object} model.ErrorResponse "请求参数错误" +// @Router /api/v1/profile [post] +func (h *ProfileHandler) Create(c *gin.Context) { + userID, ok := GetUserIDFromContext(c) + if !ok { + return + } + + var req types.CreateProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + RespondBadRequest(c, "请求参数错误: "+err.Error(), nil) + return + } + + maxProfiles := service.GetMaxProfilesPerUser() + if err := service.CheckProfileLimit(h.container.DB, userID, maxProfiles); err != nil { + RespondBadRequest(c, err.Error(), nil) + return + } + + profile, err := service.CreateProfile(h.container.DB, userID, req.Name) + if err != nil { + h.logger.Error("创建档案失败", + zap.Int64("user_id", userID), + zap.String("name", req.Name), + zap.Error(err), + ) + RespondServerError(c, err.Error(), nil) + return + } + + RespondSuccess(c, ProfileToProfileInfo(profile)) +} + +// List 获取档案列表 +// @Summary 获取档案列表 +// @Description 获取当前用户的所有档案 +// @Tags profile +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} model.Response "获取成功" +// @Router /api/v1/profile [get] +func (h *ProfileHandler) List(c *gin.Context) { + userID, ok := GetUserIDFromContext(c) + if !ok { + return + } + + profiles, err := service.GetUserProfiles(h.container.DB, userID) + if err != nil { + h.logger.Error("获取档案列表失败", + zap.Int64("user_id", userID), + zap.Error(err), + ) + RespondServerError(c, err.Error(), nil) + return + } + + RespondSuccess(c, ProfilesToProfileInfos(profiles)) +} + +// Get 获取档案详情 +// @Summary 获取档案详情 +// @Description 根据UUID获取档案详细信息 +// @Tags profile +// @Accept json +// @Produce json +// @Param uuid path string true "档案UUID" +// @Success 200 {object} model.Response "获取成功" +// @Failure 404 {object} model.ErrorResponse "档案不存在" +// @Router /api/v1/profile/{uuid} [get] +func (h *ProfileHandler) Get(c *gin.Context) { + uuid := c.Param("uuid") + if uuid == "" { + RespondBadRequest(c, "UUID不能为空", nil) + return + } + + profile, err := service.GetProfileByUUID(h.container.DB, uuid) + if err != nil { + h.logger.Error("获取档案失败", + zap.String("uuid", uuid), + zap.Error(err), + ) + RespondNotFound(c, err.Error()) + return + } + + RespondSuccess(c, ProfileToProfileInfo(profile)) +} + +// Update 更新档案 +// @Summary 更新档案 +// @Description 更新档案信息 +// @Tags profile +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param uuid path string true "档案UUID" +// @Param request body types.UpdateProfileRequest true "更新信息" +// @Success 200 {object} model.Response "更新成功" +// @Failure 403 {object} model.ErrorResponse "无权操作" +// @Router /api/v1/profile/{uuid} [put] +func (h *ProfileHandler) Update(c *gin.Context) { + userID, ok := GetUserIDFromContext(c) + if !ok { + return + } + + uuid := c.Param("uuid") + if uuid == "" { + RespondBadRequest(c, "UUID不能为空", nil) + return + } + + var req types.UpdateProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + RespondBadRequest(c, "请求参数错误: "+err.Error(), nil) + return + } + + var namePtr *string + if req.Name != "" { + namePtr = &req.Name + } + + profile, err := service.UpdateProfile(h.container.DB, uuid, userID, namePtr, req.SkinID, req.CapeID) + if err != nil { + h.logger.Error("更新档案失败", + zap.String("uuid", uuid), + zap.Int64("user_id", userID), + zap.Error(err), + ) + RespondWithError(c, err) + return + } + + RespondSuccess(c, ProfileToProfileInfo(profile)) +} + +// Delete 删除档案 +// @Summary 删除档案 +// @Description 删除指定的Minecraft档案 +// @Tags profile +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param uuid path string true "档案UUID" +// @Success 200 {object} model.Response "删除成功" +// @Failure 403 {object} model.ErrorResponse "无权操作" +// @Router /api/v1/profile/{uuid} [delete] +func (h *ProfileHandler) Delete(c *gin.Context) { + userID, ok := GetUserIDFromContext(c) + if !ok { + return + } + + uuid := c.Param("uuid") + if uuid == "" { + RespondBadRequest(c, "UUID不能为空", nil) + return + } + + if err := service.DeleteProfile(h.container.DB, uuid, userID); err != nil { + h.logger.Error("删除档案失败", + zap.String("uuid", uuid), + zap.Int64("user_id", userID), + zap.Error(err), + ) + RespondWithError(c, err) + return + } + + RespondSuccess(c, gin.H{"message": "删除成功"}) +} + +// SetActive 设置活跃档案 +// @Summary 设置活跃档案 +// @Description 将指定档案设置为活跃状态 +// @Tags profile +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param uuid path string true "档案UUID" +// @Success 200 {object} model.Response "设置成功" +// @Failure 403 {object} model.ErrorResponse "无权操作" +// @Router /api/v1/profile/{uuid}/activate [post] +func (h *ProfileHandler) SetActive(c *gin.Context) { + userID, ok := GetUserIDFromContext(c) + if !ok { + return + } + + uuid := c.Param("uuid") + if uuid == "" { + RespondBadRequest(c, "UUID不能为空", nil) + return + } + + if err := service.SetActiveProfile(h.container.DB, uuid, userID); err != nil { + h.logger.Error("设置活跃档案失败", + zap.String("uuid", uuid), + zap.Int64("user_id", userID), + zap.Error(err), + ) + RespondWithError(c, err) + return + } + + RespondSuccess(c, gin.H{"message": "设置成功"}) +} + diff --git a/internal/handler/routes_di.go b/internal/handler/routes_di.go index d022cf6..a6da9c8 100644 --- a/internal/handler/routes_di.go +++ b/internal/handler/routes_di.go @@ -10,20 +10,23 @@ 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 } // NewHandlers 创建所有Handler实例 func NewHandlers(c *container.Container) *Handlers { return &Handlers{ - Auth: NewAuthHandler(c), - User: NewUserHandler(c), - Texture: NewTextureHandler(c), + Auth: NewAuthHandler(c), + User: NewUserHandler(c), + Texture: NewTextureHandler(c), + Profile: NewProfileHandler(c), + Captcha: NewCaptchaHandler(c), + Yggdrasil: NewYggdrasilHandler(c), } } @@ -47,14 +50,14 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) { // 材质路由 registerTextureRoutes(v1, h.Texture) - // 档案路由(暂时保持原有方式) - registerProfileRoutes(v1) + // 档案路由 + registerProfileRoutesWithDI(v1, h.Profile) - // 验证码路由(暂时保持原有方式) - registerCaptchaRoutes(v1) + // 验证码路由 + registerCaptchaRoutesWithDI(v1, h.Captcha) - // Yggdrasil API路由组(暂时保持原有方式) - registerYggdrasilRoutes(v1) + // Yggdrasil API路由组 + registerYggdrasilRoutesWithDI(v1, h.Yggdrasil) // 系统路由 registerSystemRoutes(v1) @@ -115,59 +118,59 @@ func registerTextureRoutes(v1 *gin.RouterGroup, h *TextureHandler) { } } -// registerProfileRoutes 注册档案路由(保持原有方式,后续改造) -func registerProfileRoutes(v1 *gin.RouterGroup) { +// registerProfileRoutesWithDI 注册档案路由(依赖注入版本) +func registerProfileRoutesWithDI(v1 *gin.RouterGroup, h *ProfileHandler) { profileGroup := v1.Group("/profile") { // 公开路由(无需认证) - profileGroup.GET("/:uuid", GetProfile) + profileGroup.GET("/:uuid", h.Get) // 需要认证的路由 profileAuth := profileGroup.Group("") profileAuth.Use(middleware.AuthMiddleware()) { - profileAuth.POST("/", CreateProfile) - profileAuth.GET("/", GetProfiles) - profileAuth.PUT("/:uuid", UpdateProfile) - profileAuth.DELETE("/:uuid", DeleteProfile) - profileAuth.POST("/:uuid/activate", SetActiveProfile) + profileAuth.POST("/", h.Create) + profileAuth.GET("/", h.List) + profileAuth.PUT("/:uuid", h.Update) + profileAuth.DELETE("/:uuid", h.Delete) + profileAuth.POST("/:uuid/activate", h.SetActive) } } } -// registerCaptchaRoutes 注册验证码路由(保持原有方式) -func registerCaptchaRoutes(v1 *gin.RouterGroup) { +// registerCaptchaRoutesWithDI 注册验证码路由(依赖注入版本) +func registerCaptchaRoutesWithDI(v1 *gin.RouterGroup, h *CaptchaHandler) { captchaGroup := v1.Group("/captcha") { - captchaGroup.GET("/generate", Generate) - captchaGroup.POST("/verify", Verify) + captchaGroup.GET("/generate", h.Generate) + captchaGroup.POST("/verify", h.Verify) } } -// registerYggdrasilRoutes 注册Yggdrasil API路由(保持原有方式) -func registerYggdrasilRoutes(v1 *gin.RouterGroup) { +// registerYggdrasilRoutesWithDI 注册Yggdrasil API路由(依赖注入版本) +func registerYggdrasilRoutesWithDI(v1 *gin.RouterGroup, h *YggdrasilHandler) { ygg := v1.Group("/yggdrasil") { - ygg.GET("", GetMetaData) - ygg.POST("/minecraftservices/player/certificates", GetPlayerCertificates) + ygg.GET("", h.GetMetaData) + ygg.POST("/minecraftservices/player/certificates", h.GetPlayerCertificates) authserver := ygg.Group("/authserver") { - authserver.POST("/authenticate", Authenticate) - authserver.POST("/validate", ValidToken) - authserver.POST("/refresh", RefreshToken) - authserver.POST("/invalidate", InvalidToken) - authserver.POST("/signout", SignOut) + authserver.POST("/authenticate", h.Authenticate) + authserver.POST("/validate", h.ValidToken) + authserver.POST("/refresh", h.RefreshToken) + authserver.POST("/invalidate", h.InvalidToken) + authserver.POST("/signout", h.SignOut) } sessionServer := ygg.Group("/sessionserver") { - sessionServer.GET("/session/minecraft/profile/:uuid", GetProfileByUUID) - sessionServer.POST("/session/minecraft/join", JoinServer) - sessionServer.GET("/session/minecraft/hasJoined", HasJoinedServer) + sessionServer.GET("/session/minecraft/profile/:uuid", h.GetProfileByUUID) + sessionServer.POST("/session/minecraft/join", h.JoinServer) + sessionServer.GET("/session/minecraft/hasJoined", h.HasJoinedServer) } api := ygg.Group("/api") profiles := api.Group("/profiles") { - profiles.POST("/minecraft", GetProfilesByName) + profiles.POST("/minecraft", h.GetProfilesByName) } } } @@ -188,4 +191,3 @@ func registerSystemRoutes(v1 *gin.RouterGroup) { }) } } - diff --git a/internal/handler/yggdrasil_handler_di.go b/internal/handler/yggdrasil_handler_di.go new file mode 100644 index 0000000..c4fb8f3 --- /dev/null +++ b/internal/handler/yggdrasil_handler_di.go @@ -0,0 +1,454 @@ +package handler + +import ( + "bytes" + "carrotskin/internal/container" + "carrotskin/internal/model" + "carrotskin/internal/service" + "carrotskin/pkg/utils" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// YggdrasilHandler Yggdrasil API处理器 +type YggdrasilHandler struct { + container *container.Container + logger *zap.Logger +} + +// NewYggdrasilHandler 创建YggdrasilHandler实例 +func NewYggdrasilHandler(c *container.Container) *YggdrasilHandler { + return &YggdrasilHandler{ + container: c, + logger: c.Logger, + } +} + +// Authenticate 用户认证 +func (h *YggdrasilHandler) Authenticate(c *gin.Context) { + rawData, err := io.ReadAll(c.Request.Body) + if err != nil { + h.logger.Error("读取请求体失败", zap.Error(err)) + c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败"}) + return + } + c.Request.Body = io.NopCloser(bytes.NewBuffer(rawData)) + + var request AuthenticateRequest + if err = c.ShouldBindJSON(&request); err != nil { + h.logger.Error("解析认证请求失败", zap.Error(err)) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var userId int64 + var profile *model.Profile + var UUID string + + if emailRegex.MatchString(request.Identifier) { + userId, err = service.GetUserIDByEmail(h.container.DB, request.Identifier) + } else { + profile, err = service.GetProfileByProfileName(h.container.DB, request.Identifier) + if err != nil { + h.logger.Error("用户名不存在", zap.String("identifier", request.Identifier), zap.Error(err)) + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + return + } + userId = profile.UserID + UUID = profile.UUID + } + + if err != nil { + h.logger.Warn("认证失败: 用户不存在", zap.String("identifier", request.Identifier), zap.Error(err)) + c.JSON(http.StatusForbidden, gin.H{"error": "用户不存在"}) + return + } + + if err := service.VerifyPassword(h.container.DB, request.Password, userId); err != nil { + h.logger.Warn("认证失败: 密码错误", zap.Error(err)) + c.JSON(http.StatusForbidden, gin.H{"error": ErrWrongPassword}) + return + } + + selectedProfile, availableProfiles, accessToken, clientToken, err := service.NewToken(h.container.DB, h.logger, userId, UUID, request.ClientToken) + if err != nil { + h.logger.Error("生成令牌失败", zap.Error(err), zap.Int64("userId", userId)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + user, err := service.GetUserByID(userId) + if err != nil { + h.logger.Error("获取用户信息失败", zap.Error(err), zap.Int64("userId", userId)) + } + + availableProfilesData := make([]map[string]interface{}, 0, len(availableProfiles)) + for _, p := range availableProfiles { + availableProfilesData = append(availableProfilesData, service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *p)) + } + + response := AuthenticateResponse{ + AccessToken: accessToken, + ClientToken: clientToken, + AvailableProfiles: availableProfilesData, + } + + if selectedProfile != nil { + response.SelectedProfile = service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *selectedProfile) + } + + if request.RequestUser && user != nil { + response.User = service.SerializeUser(h.logger, user, UUID) + } + + h.logger.Info("用户认证成功", zap.Int64("userId", userId)) + c.JSON(http.StatusOK, response) +} + +// ValidToken 验证令牌 +func (h *YggdrasilHandler) ValidToken(c *gin.Context) { + var request ValidTokenRequest + if err := c.ShouldBindJSON(&request); err != nil { + h.logger.Error("解析验证令牌请求失败", zap.Error(err)) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if service.ValidToken(h.container.DB, request.AccessToken, request.ClientToken) { + h.logger.Info("令牌验证成功", zap.String("accessToken", request.AccessToken)) + c.JSON(http.StatusNoContent, gin.H{"valid": true}) + } else { + h.logger.Warn("令牌验证失败", zap.String("accessToken", request.AccessToken)) + c.JSON(http.StatusForbidden, gin.H{"valid": false}) + } +} + +// RefreshToken 刷新令牌 +func (h *YggdrasilHandler) RefreshToken(c *gin.Context) { + var request RefreshRequest + if err := c.ShouldBindJSON(&request); err != nil { + h.logger.Error("解析刷新令牌请求失败", zap.Error(err)) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + UUID, err := service.GetUUIDByAccessToken(h.container.DB, request.AccessToken) + if err != nil { + h.logger.Warn("刷新令牌失败: 无效的访问令牌", zap.String("token", request.AccessToken), zap.Error(err)) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID, _ := service.GetUserIDByAccessToken(h.container.DB, request.AccessToken) + UUID = utils.FormatUUID(UUID) + + profile, err := service.GetProfileByUUID(h.container.DB, UUID) + if err != nil { + h.logger.Error("刷新令牌失败: 无法获取用户信息", zap.Error(err)) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var profileData map[string]interface{} + var userData map[string]interface{} + var profileID string + + if request.SelectedProfile != nil { + profileIDValue, ok := request.SelectedProfile["id"] + if !ok { + h.logger.Error("刷新令牌失败: 缺少配置文件ID", zap.Int64("userId", userID)) + c.JSON(http.StatusBadRequest, gin.H{"error": "缺少配置文件ID"}) + return + } + + profileID, ok = profileIDValue.(string) + if !ok { + h.logger.Error("刷新令牌失败: 配置文件ID类型错误", zap.Int64("userId", userID)) + c.JSON(http.StatusBadRequest, gin.H{"error": "配置文件ID必须是字符串"}) + return + } + + profileID = utils.FormatUUID(profileID) + + if profile.UserID != userID { + h.logger.Warn("刷新令牌失败: 用户不匹配", + zap.Int64("userId", userID), + zap.Int64("profileUserId", profile.UserID), + ) + c.JSON(http.StatusBadRequest, gin.H{"error": ErrUserNotMatch}) + return + } + + profileData = service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *profile) + } + + user, _ := service.GetUserByID(userID) + if request.RequestUser && user != nil { + userData = service.SerializeUser(h.logger, user, UUID) + } + + newAccessToken, newClientToken, err := service.RefreshToken(h.container.DB, h.logger, + request.AccessToken, + request.ClientToken, + profileID, + ) + if err != nil { + h.logger.Error("刷新令牌失败", zap.Error(err), zap.Int64("userId", userID)) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + h.logger.Info("刷新令牌成功", zap.Int64("userId", userID)) + c.JSON(http.StatusOK, RefreshResponse{ + AccessToken: newAccessToken, + ClientToken: newClientToken, + SelectedProfile: profileData, + User: userData, + }) +} + +// InvalidToken 使令牌失效 +func (h *YggdrasilHandler) InvalidToken(c *gin.Context) { + var request ValidTokenRequest + if err := c.ShouldBindJSON(&request); err != nil { + h.logger.Error("解析使令牌失效请求失败", zap.Error(err)) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + service.InvalidToken(h.container.DB, h.logger, request.AccessToken) + h.logger.Info("令牌已失效", zap.String("token", request.AccessToken)) + c.JSON(http.StatusNoContent, gin.H{}) +} + +// SignOut 用户登出 +func (h *YggdrasilHandler) SignOut(c *gin.Context) { + var request SignOutRequest + if err := c.ShouldBindJSON(&request); err != nil { + h.logger.Error("解析登出请求失败", zap.Error(err)) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if !emailRegex.MatchString(request.Email) { + h.logger.Warn("登出失败: 邮箱格式不正确", zap.String("email", request.Email)) + c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidEmailFormat}) + return + } + + user, err := service.GetUserByEmail(request.Email) + if err != nil || user == nil { + h.logger.Warn("登出失败: 用户不存在", zap.String("email", request.Email), zap.Error(err)) + c.JSON(http.StatusBadRequest, gin.H{"error": "用户不存在"}) + return + } + + if err := service.VerifyPassword(h.container.DB, request.Password, user.ID); err != nil { + h.logger.Warn("登出失败: 密码错误", zap.Int64("userId", user.ID)) + c.JSON(http.StatusBadRequest, gin.H{"error": ErrWrongPassword}) + return + } + + service.InvalidUserTokens(h.container.DB, h.logger, user.ID) + h.logger.Info("用户登出成功", zap.Int64("userId", user.ID)) + c.JSON(http.StatusNoContent, gin.H{"valid": true}) +} + +// GetProfileByUUID 根据UUID获取档案 +func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) { + uuid := utils.FormatUUID(c.Param("uuid")) + h.logger.Info("获取配置文件请求", zap.String("uuid", uuid)) + + profile, err := service.GetProfileByUUID(h.container.DB, uuid) + if err != nil { + h.logger.Error("获取配置文件失败", zap.Error(err), zap.String("uuid", uuid)) + standardResponse(c, http.StatusInternalServerError, nil, err.Error()) + return + } + + h.logger.Info("成功获取配置文件", zap.String("uuid", uuid), zap.String("name", profile.Name)) + c.JSON(http.StatusOK, service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *profile)) +} + +// JoinServer 加入服务器 +func (h *YggdrasilHandler) JoinServer(c *gin.Context) { + var request JoinServerRequest + clientIP := c.ClientIP() + + if err := c.ShouldBindJSON(&request); err != nil { + h.logger.Error("解析加入服务器请求失败", zap.Error(err), zap.String("ip", clientIP)) + standardResponse(c, http.StatusBadRequest, nil, ErrInvalidRequest) + return + } + + h.logger.Info("收到加入服务器请求", + zap.String("serverId", request.ServerID), + zap.String("userUUID", request.SelectedProfile), + zap.String("ip", clientIP), + ) + + if err := service.JoinServer(h.container.DB, h.logger, h.container.Redis, request.ServerID, request.AccessToken, request.SelectedProfile, clientIP); err != nil { + h.logger.Error("加入服务器失败", + zap.Error(err), + zap.String("serverId", request.ServerID), + zap.String("userUUID", request.SelectedProfile), + zap.String("ip", clientIP), + ) + standardResponse(c, http.StatusInternalServerError, nil, ErrJoinServerFailed) + return + } + + h.logger.Info("加入服务器成功", + zap.String("serverId", request.ServerID), + zap.String("userUUID", request.SelectedProfile), + zap.String("ip", clientIP), + ) + c.Status(http.StatusNoContent) +} + +// HasJoinedServer 验证玩家是否已加入服务器 +func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) { + clientIP, _ := c.GetQuery("ip") + + serverID, exists := c.GetQuery("serverId") + if !exists || serverID == "" { + h.logger.Warn("缺少服务器ID参数", zap.String("ip", clientIP)) + standardResponse(c, http.StatusNoContent, nil, ErrServerIDRequired) + return + } + + username, exists := c.GetQuery("username") + if !exists || username == "" { + h.logger.Warn("缺少用户名参数", zap.String("serverId", serverID), zap.String("ip", clientIP)) + standardResponse(c, http.StatusNoContent, nil, ErrUsernameRequired) + return + } + + h.logger.Info("收到会话验证请求", + zap.String("serverId", serverID), + zap.String("username", username), + zap.String("ip", clientIP), + ) + + if err := service.HasJoinedServer(h.logger, h.container.Redis, serverID, username, clientIP); err != nil { + h.logger.Warn("会话验证失败", + zap.Error(err), + zap.String("serverId", serverID), + zap.String("username", username), + zap.String("ip", clientIP), + ) + standardResponse(c, http.StatusNoContent, nil, ErrSessionVerifyFailed) + return + } + + profile, err := service.GetProfileByUUID(h.container.DB, username) + if err != nil { + h.logger.Error("获取用户配置文件失败", zap.Error(err), zap.String("username", username)) + standardResponse(c, http.StatusNoContent, nil, ErrProfileNotFound) + return + } + + h.logger.Info("会话验证成功", + zap.String("serverId", serverID), + zap.String("username", username), + zap.String("uuid", profile.UUID), + ) + c.JSON(200, service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *profile)) +} + +// GetProfilesByName 批量获取配置文件 +func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) { + var names []string + + if err := c.ShouldBindJSON(&names); err != nil { + h.logger.Error("解析名称数组请求失败", zap.Error(err)) + standardResponse(c, http.StatusBadRequest, nil, ErrInvalidParams) + return + } + + h.logger.Info("接收到批量获取配置文件请求", zap.Int("count", len(names))) + + profiles, err := service.GetProfilesDataByNames(h.container.DB, names) + if err != nil { + h.logger.Error("获取配置文件失败", zap.Error(err)) + } + + h.logger.Info("成功获取配置文件", zap.Int("requested", len(names)), zap.Int("returned", len(profiles))) + c.JSON(http.StatusOK, profiles) +} + +// GetMetaData 获取Yggdrasil元数据 +func (h *YggdrasilHandler) GetMetaData(c *gin.Context) { + meta := gin.H{ + "implementationName": "CellAuth", + "implementationVersion": "0.0.1", + "serverName": "LittleLan's Yggdrasil Server Implementation.", + "links": gin.H{ + "homepage": "https://skin.littlelan.cn", + "register": "https://skin.littlelan.cn/auth", + }, + "feature.non_email_login": true, + "feature.enable_profile_key": true, + } + + skinDomains := []string{".hitwh.games", ".littlelan.cn"} + signature, err := service.GetPublicKeyFromRedisFunc(h.logger, h.container.Redis) + if err != nil { + h.logger.Error("获取公钥失败", zap.Error(err)) + standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer) + return + } + + h.logger.Info("提供元数据") + c.JSON(http.StatusOK, gin.H{ + "meta": meta, + "skinDomains": skinDomains, + "signaturePublickey": signature, + }) +} + +// GetPlayerCertificates 获取玩家证书 +func (h *YggdrasilHandler) GetPlayerCertificates(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header not provided"}) + c.Abort() + return + } + + bearerPrefix := "Bearer " + if len(authHeader) < len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"}) + c.Abort() + return + } + + tokenID := authHeader[len(bearerPrefix):] + if tokenID == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"}) + c.Abort() + return + } + + uuid, err := service.GetUUIDByAccessToken(h.container.DB, tokenID) + if uuid == "" { + h.logger.Error("获取玩家UUID失败", zap.Error(err)) + standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer) + return + } + + uuid = utils.FormatUUID(uuid) + + certificate, err := service.GeneratePlayerCertificate(h.container.DB, h.logger, h.container.Redis, uuid) + if err != nil { + h.logger.Error("生成玩家证书失败", zap.Error(err)) + standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer) + return + } + + h.logger.Info("成功生成玩家证书") + c.JSON(http.StatusOK, certificate) +} diff --git a/internal/repository/interfaces.go b/internal/repository/interfaces.go index f72ca88..8fabb7c 100644 --- a/internal/repository/interfaces.go +++ b/internal/repository/interfaces.go @@ -82,4 +82,3 @@ type YggdrasilRepository interface { GetPasswordByID(id int64) (string, error) ResetPassword(id int64, password string) error } - diff --git a/internal/repository/profile_repository_impl.go b/internal/repository/profile_repository_impl.go index ebe3fdb..5eb4e9e 100644 --- a/internal/repository/profile_repository_impl.go +++ b/internal/repository/profile_repository_impl.go @@ -146,4 +146,3 @@ func (r *profileRepositoryImpl) UpdateKeyPair(profileId string, keyPair *model.K return nil }) } - diff --git a/internal/repository/texture_repository_impl.go b/internal/repository/texture_repository_impl.go index c6a2971..82f37df 100644 --- a/internal/repository/texture_repository_impl.go +++ b/internal/repository/texture_repository_impl.go @@ -172,4 +172,3 @@ func (r *textureRepositoryImpl) CountByUploaderID(uploaderID int64) (int64, erro Count(&count).Error return count, err } - diff --git a/internal/repository/token_repository_impl.go b/internal/repository/token_repository_impl.go index e4c94e1..623f06a 100644 --- a/internal/repository/token_repository_impl.go +++ b/internal/repository/token_repository_impl.go @@ -68,4 +68,3 @@ func (r *tokenRepositoryImpl) BatchDelete(accessTokens []string) (int64, error) result := r.db.Where("access_token IN ?", accessTokens).Delete(&model.Token{}) return result.RowsAffected, result.Error } - diff --git a/internal/repository/user_repository_impl.go b/internal/repository/user_repository_impl.go index 57ec4c8..b932ae7 100644 --- a/internal/repository/user_repository_impl.go +++ b/internal/repository/user_repository_impl.go @@ -100,4 +100,3 @@ func handleNotFoundResult[T any](result *T, err error) (*T, error) { } return result, nil } -