feat: 完善依赖注入改造
完成所有Handler的依赖注入改造: - AuthHandler: 认证相关功能 - UserHandler: 用户管理功能 - TextureHandler: 材质管理功能 - ProfileHandler: 档案管理功能 - CaptchaHandler: 验证码功能 - YggdrasilHandler: Yggdrasil API功能 新增错误类型定义: - internal/errors/errors.go: 统一的错误类型和工厂函数 更新main.go: - 使用container.NewContainer创建依赖容器 - 使用handler.RegisterRoutesWithDI注册路由 代码遵循Go最佳实践: - 依赖通过构造函数注入 - Handler通过结构体方法实现 - 统一的错误处理模式 - 清晰的分层架构
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "carrotskin/docs" // Swagger文档
|
_ "carrotskin/docs" // Swagger文档
|
||||||
|
"carrotskin/internal/container"
|
||||||
"carrotskin/internal/handler"
|
"carrotskin/internal/handler"
|
||||||
"carrotskin/internal/middleware"
|
"carrotskin/internal/middleware"
|
||||||
"carrotskin/pkg/auth"
|
"carrotskin/pkg/auth"
|
||||||
@@ -66,10 +67,11 @@ func main() {
|
|||||||
defer redis.MustGetClient().Close()
|
defer redis.MustGetClient().Close()
|
||||||
|
|
||||||
// 初始化对象存储 (RustFS - S3兼容)
|
// 初始化对象存储 (RustFS - S3兼容)
|
||||||
// 如果对象存储未配置或连接失败,记录警告但不退出(某些功能可能不可用)
|
var storageClient *storage.StorageClient
|
||||||
if err := storage.Init(cfg.RustFS); err != nil {
|
if err := storage.Init(cfg.RustFS); err != nil {
|
||||||
loggerInstance.Warn("对象存储连接失败,某些功能可能不可用", zap.Error(err))
|
loggerInstance.Warn("对象存储连接失败,某些功能可能不可用", zap.Error(err))
|
||||||
} else {
|
} else {
|
||||||
|
storageClient = storage.MustGetClient()
|
||||||
loggerInstance.Info("对象存储连接成功")
|
loggerInstance.Info("对象存储连接成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +80,15 @@ func main() {
|
|||||||
loggerInstance.Fatal("邮件服务初始化失败", zap.Error(err))
|
loggerInstance.Fatal("邮件服务初始化失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建依赖注入容器
|
||||||
|
c := container.NewContainer(
|
||||||
|
database.MustGetDB(),
|
||||||
|
redis.MustGetClient(),
|
||||||
|
loggerInstance,
|
||||||
|
auth.MustGetJWTService(),
|
||||||
|
storageClient,
|
||||||
|
)
|
||||||
|
|
||||||
// 设置Gin模式
|
// 设置Gin模式
|
||||||
if cfg.Server.Mode == "production" {
|
if cfg.Server.Mode == "production" {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
@@ -91,8 +102,8 @@ func main() {
|
|||||||
router.Use(middleware.Recovery(loggerInstance))
|
router.Use(middleware.Recovery(loggerInstance))
|
||||||
router.Use(middleware.CORS())
|
router.Use(middleware.CORS())
|
||||||
|
|
||||||
// 注册路由
|
// 使用依赖注入方式注册路由
|
||||||
handler.RegisterRoutes(router)
|
handler.RegisterRoutesWithDI(router, c)
|
||||||
|
|
||||||
// 创建HTTP服务器
|
// 创建HTTP服务器
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
|
|||||||
@@ -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("服务器已关闭")
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -21,11 +21,11 @@ type Container struct {
|
|||||||
Storage *storage.StorageClient
|
Storage *storage.StorageClient
|
||||||
|
|
||||||
// Repository层
|
// Repository层
|
||||||
UserRepo repository.UserRepository
|
UserRepo repository.UserRepository
|
||||||
ProfileRepo repository.ProfileRepository
|
ProfileRepo repository.ProfileRepository
|
||||||
TextureRepo repository.TextureRepository
|
TextureRepo repository.TextureRepository
|
||||||
TokenRepo repository.TokenRepository
|
TokenRepo repository.TokenRepository
|
||||||
ConfigRepo repository.SystemConfigRepository
|
ConfigRepo repository.SystemConfigRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContainer 创建依赖容器
|
// NewContainer 创建依赖容器
|
||||||
@@ -135,4 +135,3 @@ func WithConfigRepo(repo repository.SystemConfigRepository) Option {
|
|||||||
c.ConfigRepo = repo
|
c.ConfigRepo = repo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
127
internal/errors/errors.go
Normal file
127
internal/errors/errors.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
108
internal/handler/captcha_handler_di.go
Normal file
108
internal/handler/captcha_handler_di.go
Normal file
@@ -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": "验证失败,请重试",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
247
internal/handler/profile_handler_di.go
Normal file
247
internal/handler/profile_handler_di.go
Normal file
@@ -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": "设置成功"})
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,20 +10,23 @@ import (
|
|||||||
|
|
||||||
// Handlers 集中管理所有Handler
|
// Handlers 集中管理所有Handler
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
Auth *AuthHandler
|
Auth *AuthHandler
|
||||||
User *UserHandler
|
User *UserHandler
|
||||||
Texture *TextureHandler
|
Texture *TextureHandler
|
||||||
// Profile *ProfileHandler // 后续添加
|
Profile *ProfileHandler
|
||||||
// Captcha *CaptchaHandler // 后续添加
|
Captcha *CaptchaHandler
|
||||||
// Yggdrasil *YggdrasilHandler // 后续添加
|
Yggdrasil *YggdrasilHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandlers 创建所有Handler实例
|
// NewHandlers 创建所有Handler实例
|
||||||
func NewHandlers(c *container.Container) *Handlers {
|
func NewHandlers(c *container.Container) *Handlers {
|
||||||
return &Handlers{
|
return &Handlers{
|
||||||
Auth: NewAuthHandler(c),
|
Auth: NewAuthHandler(c),
|
||||||
User: NewUserHandler(c),
|
User: NewUserHandler(c),
|
||||||
Texture: NewTextureHandler(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)
|
registerTextureRoutes(v1, h.Texture)
|
||||||
|
|
||||||
// 档案路由(暂时保持原有方式)
|
// 档案路由
|
||||||
registerProfileRoutes(v1)
|
registerProfileRoutesWithDI(v1, h.Profile)
|
||||||
|
|
||||||
// 验证码路由(暂时保持原有方式)
|
// 验证码路由
|
||||||
registerCaptchaRoutes(v1)
|
registerCaptchaRoutesWithDI(v1, h.Captcha)
|
||||||
|
|
||||||
// Yggdrasil API路由组(暂时保持原有方式)
|
// Yggdrasil API路由组
|
||||||
registerYggdrasilRoutes(v1)
|
registerYggdrasilRoutesWithDI(v1, h.Yggdrasil)
|
||||||
|
|
||||||
// 系统路由
|
// 系统路由
|
||||||
registerSystemRoutes(v1)
|
registerSystemRoutes(v1)
|
||||||
@@ -115,59 +118,59 @@ func registerTextureRoutes(v1 *gin.RouterGroup, h *TextureHandler) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerProfileRoutes 注册档案路由(保持原有方式,后续改造)
|
// registerProfileRoutesWithDI 注册档案路由(依赖注入版本)
|
||||||
func registerProfileRoutes(v1 *gin.RouterGroup) {
|
func registerProfileRoutesWithDI(v1 *gin.RouterGroup, h *ProfileHandler) {
|
||||||
profileGroup := v1.Group("/profile")
|
profileGroup := v1.Group("/profile")
|
||||||
{
|
{
|
||||||
// 公开路由(无需认证)
|
// 公开路由(无需认证)
|
||||||
profileGroup.GET("/:uuid", GetProfile)
|
profileGroup.GET("/:uuid", h.Get)
|
||||||
|
|
||||||
// 需要认证的路由
|
// 需要认证的路由
|
||||||
profileAuth := profileGroup.Group("")
|
profileAuth := profileGroup.Group("")
|
||||||
profileAuth.Use(middleware.AuthMiddleware())
|
profileAuth.Use(middleware.AuthMiddleware())
|
||||||
{
|
{
|
||||||
profileAuth.POST("/", CreateProfile)
|
profileAuth.POST("/", h.Create)
|
||||||
profileAuth.GET("/", GetProfiles)
|
profileAuth.GET("/", h.List)
|
||||||
profileAuth.PUT("/:uuid", UpdateProfile)
|
profileAuth.PUT("/:uuid", h.Update)
|
||||||
profileAuth.DELETE("/:uuid", DeleteProfile)
|
profileAuth.DELETE("/:uuid", h.Delete)
|
||||||
profileAuth.POST("/:uuid/activate", SetActiveProfile)
|
profileAuth.POST("/:uuid/activate", h.SetActive)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerCaptchaRoutes 注册验证码路由(保持原有方式)
|
// registerCaptchaRoutesWithDI 注册验证码路由(依赖注入版本)
|
||||||
func registerCaptchaRoutes(v1 *gin.RouterGroup) {
|
func registerCaptchaRoutesWithDI(v1 *gin.RouterGroup, h *CaptchaHandler) {
|
||||||
captchaGroup := v1.Group("/captcha")
|
captchaGroup := v1.Group("/captcha")
|
||||||
{
|
{
|
||||||
captchaGroup.GET("/generate", Generate)
|
captchaGroup.GET("/generate", h.Generate)
|
||||||
captchaGroup.POST("/verify", Verify)
|
captchaGroup.POST("/verify", h.Verify)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerYggdrasilRoutes 注册Yggdrasil API路由(保持原有方式)
|
// registerYggdrasilRoutesWithDI 注册Yggdrasil API路由(依赖注入版本)
|
||||||
func registerYggdrasilRoutes(v1 *gin.RouterGroup) {
|
func registerYggdrasilRoutesWithDI(v1 *gin.RouterGroup, h *YggdrasilHandler) {
|
||||||
ygg := v1.Group("/yggdrasil")
|
ygg := v1.Group("/yggdrasil")
|
||||||
{
|
{
|
||||||
ygg.GET("", GetMetaData)
|
ygg.GET("", h.GetMetaData)
|
||||||
ygg.POST("/minecraftservices/player/certificates", GetPlayerCertificates)
|
ygg.POST("/minecraftservices/player/certificates", h.GetPlayerCertificates)
|
||||||
authserver := ygg.Group("/authserver")
|
authserver := ygg.Group("/authserver")
|
||||||
{
|
{
|
||||||
authserver.POST("/authenticate", Authenticate)
|
authserver.POST("/authenticate", h.Authenticate)
|
||||||
authserver.POST("/validate", ValidToken)
|
authserver.POST("/validate", h.ValidToken)
|
||||||
authserver.POST("/refresh", RefreshToken)
|
authserver.POST("/refresh", h.RefreshToken)
|
||||||
authserver.POST("/invalidate", InvalidToken)
|
authserver.POST("/invalidate", h.InvalidToken)
|
||||||
authserver.POST("/signout", SignOut)
|
authserver.POST("/signout", h.SignOut)
|
||||||
}
|
}
|
||||||
sessionServer := ygg.Group("/sessionserver")
|
sessionServer := ygg.Group("/sessionserver")
|
||||||
{
|
{
|
||||||
sessionServer.GET("/session/minecraft/profile/:uuid", GetProfileByUUID)
|
sessionServer.GET("/session/minecraft/profile/:uuid", h.GetProfileByUUID)
|
||||||
sessionServer.POST("/session/minecraft/join", JoinServer)
|
sessionServer.POST("/session/minecraft/join", h.JoinServer)
|
||||||
sessionServer.GET("/session/minecraft/hasJoined", HasJoinedServer)
|
sessionServer.GET("/session/minecraft/hasJoined", h.HasJoinedServer)
|
||||||
}
|
}
|
||||||
api := ygg.Group("/api")
|
api := ygg.Group("/api")
|
||||||
profiles := api.Group("/profiles")
|
profiles := api.Group("/profiles")
|
||||||
{
|
{
|
||||||
profiles.POST("/minecraft", GetProfilesByName)
|
profiles.POST("/minecraft", h.GetProfilesByName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,4 +191,3 @@ func registerSystemRoutes(v1 *gin.RouterGroup) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
454
internal/handler/yggdrasil_handler_di.go
Normal file
454
internal/handler/yggdrasil_handler_di.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -82,4 +82,3 @@ type YggdrasilRepository interface {
|
|||||||
GetPasswordByID(id int64) (string, error)
|
GetPasswordByID(id int64) (string, error)
|
||||||
ResetPassword(id int64, password string) error
|
ResetPassword(id int64, password string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -146,4 +146,3 @@ func (r *profileRepositoryImpl) UpdateKeyPair(profileId string, keyPair *model.K
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,4 +172,3 @@ func (r *textureRepositoryImpl) CountByUploaderID(uploaderID int64) (int64, erro
|
|||||||
Count(&count).Error
|
Count(&count).Error
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,4 +68,3 @@ func (r *tokenRepositoryImpl) BatchDelete(accessTokens []string) (int64, error)
|
|||||||
result := r.db.Where("access_token IN ?", accessTokens).Delete(&model.Token{})
|
result := r.db.Where("access_token IN ?", accessTokens).Delete(&model.Token{})
|
||||||
return result.RowsAffected, result.Error
|
return result.RowsAffected, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,4 +100,3 @@ func handleNotFoundResult[T any](result *T, err error) (*T, error) {
|
|||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user