From 7d1c78f96501d0bfe842944bd82c07efedb4ec8d Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Mon, 15 Jun 2026 16:40:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E5=8D=95=E4=BE=8B=E3=80=81=E4=BF=AE=E5=A4=8D=E5=88=86?= =?UTF-8?q?=E5=B1=82=E8=BF=9D=E8=A7=84=E3=80=81=E7=BB=9F=E4=B8=80=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E4=B8=8E=E5=93=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/container/container.go | 159 +-------- internal/errors/errors.go | 18 +- internal/errors/errors_test.go | 18 +- internal/handler/admin_handler.go | 113 ++++--- internal/handler/auth_handler.go | 6 - internal/handler/customskin_handler.go | 8 +- internal/handler/helpers.go | 27 +- internal/handler/routes.go | 6 +- internal/handler/swagger.go | 84 +++-- internal/handler/swagger_test.go | 6 +- internal/handler/yggdrasil_handler.go | 2 +- internal/middleware/casbin.go | 64 ++-- internal/middleware/cors.go | 16 +- internal/middleware/cors_test.go | 21 +- internal/repository/interfaces.go | 5 + internal/repository/texture_repository.go | 24 ++ internal/repository/user_repository.go | 17 + internal/repository/yggdrasil_repository.go | 5 + internal/service/captcha_service.go | 13 +- internal/service/helpers.go | 27 +- internal/service/helpers_test.go | 19 -- internal/service/interfaces.go | 10 + internal/service/mocks_test.go | 44 +++ internal/service/profile_service.go | 11 +- internal/service/signature_service.go | 18 +- internal/service/texture_service.go | 99 ++++-- internal/service/texture_service_test.go | 12 +- internal/service/token_service_redis.go | 8 +- internal/service/user_service.go | 79 +++-- internal/service/user_service_test.go | 19 +- internal/service/verification_service.go | 16 +- internal/service/yggdrasil_auth_service.go | 7 +- .../service/yggdrasil_certificate_service.go | 4 +- .../yggdrasil_serialization_service.go | 2 +- .../service/yggdrasil_service_composite.go | 8 +- internal/types/common.go | 19 +- internal/types/common_test.go | 314 +----------------- pkg/auth/manager.go | 50 +-- pkg/auth/manager_test.go | 86 ----- pkg/config/manager.go | 75 +---- pkg/config/manager_test.go | 74 +---- pkg/database/cache.go | 6 +- pkg/database/manager.go | 79 +---- pkg/database/manager_sqlite_test.go | 26 +- pkg/database/manager_test.go | 87 ----- pkg/database/seed.go | 9 +- pkg/email/email_test.go | 17 +- pkg/email/manager.go | 58 +--- pkg/email/manager_test.go | 69 ---- pkg/logger/manager.go | 61 +--- pkg/logger/manager_test.go | 47 --- pkg/redis/manager.go | 101 +----- pkg/redis/manager_test.go | 53 --- pkg/storage/manager.go | 59 +--- pkg/storage/manager_test.go | 52 --- 55 files changed, 593 insertions(+), 1744 deletions(-) delete mode 100644 pkg/auth/manager_test.go delete mode 100644 pkg/database/manager_test.go delete mode 100644 pkg/email/manager_test.go delete mode 100644 pkg/logger/manager_test.go delete mode 100644 pkg/redis/manager_test.go delete mode 100644 pkg/storage/manager_test.go diff --git a/internal/container/container.go b/internal/container/container.go index c1ee0e9..3725de7 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -4,10 +4,12 @@ import ( "carrotskin/internal/repository" "carrotskin/internal/service" "carrotskin/pkg/auth" + "carrotskin/pkg/config" "carrotskin/pkg/database" "carrotskin/pkg/email" "carrotskin/pkg/redis" "carrotskin/pkg/storage" + "context" "time" "go.uber.org/zap" @@ -17,6 +19,9 @@ import ( // Container 依赖注入容器 // 集中管理所有依赖,便于测试和维护 type Container struct { + // 配置 + Config *config.Config + // 基础设施依赖 DB *gorm.DB Redis *redis.Client @@ -40,20 +45,20 @@ type Container struct { TokenService service.TokenService YggdrasilService service.YggdrasilService VerificationService service.VerificationService - SecurityService service.SecurityService CaptchaService service.CaptchaService SignatureService *service.SignatureService } // NewContainer 创建依赖容器 func NewContainer( + cfg *config.Config, db *gorm.DB, redisClient *redis.Client, logger *zap.Logger, jwtService *auth.JWTService, casbinService *auth.CasbinService, storageClient *storage.StorageClient, - emailService interface{}, // 接受 email.Service 但使用 interface{} 避免循环依赖 + emailService *email.Service, ) *Container { // 创建缓存管理器 cacheManager := database.NewCacheManager(redisClient, database.CacheConfig{ @@ -71,6 +76,7 @@ func NewContainer( }) c := &Container{ + Config: cfg, DB: db, Redis: redisClient, Logger: logger, @@ -92,14 +98,17 @@ func NewContainer( c.SignatureService = service.NewSignatureService(c.ProfileRepo, redisClient, logger) // 初始化Service(注入缓存管理器) - c.UserService = service.NewUserService(c.UserRepo, jwtService, redisClient, cacheManager, storageClient, logger) + c.UserService = service.NewUserService(cfg, c.UserRepo, jwtService, redisClient, cacheManager, storageClient, logger) c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, cacheManager, logger) - c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, storageClient, cacheManager, logger) + c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, storageClient, cacheManager, logger, db) // 获取Yggdrasil私钥并创建JWT服务(TokenService需要) // 注意:这里仍然需要预先初始化,因为TokenService在创建时需要YggdrasilJWT // 但SignatureService已经作为依赖注入,降低了耦合度 - _, privateKey, err := c.SignatureService.GetOrCreateYggdrasilKeyPair() + // 使用后台 ctx(启动阶段不依赖请求 ctx) + initCtx, cancelInit := context.WithTimeout(context.Background(), 30*time.Second) + _, privateKey, err := c.SignatureService.GetOrCreateYggdrasilKeyPair(initCtx) + cancelInit() if err != nil { logger.Fatal("获取Yggdrasil私钥失败", zap.Error(err)) } @@ -121,149 +130,15 @@ func NewContainer( c.TokenService = service.NewTokenServiceRedis(tokenStore, c.ClientRepo, c.ProfileRepo, yggdrasilJWT, logger) // 使用组合服务(内部包含认证、会话、序列化、证书服务) - c.YggdrasilService = service.NewYggdrasilServiceComposite(db, c.UserRepo, c.ProfileRepo, c.YggdrasilRepo, c.SignatureService, redisClient, logger, c.TokenService) + c.YggdrasilService = service.NewYggdrasilServiceComposite(c.UserRepo, c.ProfileRepo, c.YggdrasilRepo, c.TextureRepo, c.SignatureService, redisClient, logger, c.TokenService) // 初始化其他服务 - c.SecurityService = service.NewSecurityService(redisClient) - c.CaptchaService = service.NewCaptchaService(redisClient, logger) + c.CaptchaService = service.NewCaptchaService(cfg, redisClient, logger) // 初始化VerificationService(需要email.Service) if emailService != nil { - if emailSvc, ok := emailService.(*email.Service); ok { - c.VerificationService = service.NewVerificationService(redisClient, emailSvc) - } + c.VerificationService = service.NewVerificationService(cfg, redisClient, emailService) } return c } - -// NewTestContainer 创建测试用容器(可注入mock依赖) -func NewTestContainer(opts ...Option) *Container { - c := &Container{} - for _, opt := range opts { - opt(c) - } - return c -} - -// Option 容器配置选项 -type Option func(*Container) - -// WithDB 设置数据库连接 -func WithDB(db *gorm.DB) Option { - return func(c *Container) { - c.DB = db - } -} - -// WithRedis 设置Redis客户端 -func WithRedis(redis *redis.Client) Option { - return func(c *Container) { - c.Redis = redis - } -} - -// WithLogger 设置日志 -func WithLogger(logger *zap.Logger) Option { - return func(c *Container) { - c.Logger = logger - } -} - -// WithJWT 设置JWT服务 -func WithJWT(jwt *auth.JWTService) Option { - return func(c *Container) { - c.JWT = jwt - } -} - -// WithStorage 设置存储客户端 -func WithStorage(storage *storage.StorageClient) Option { - return func(c *Container) { - c.Storage = storage - } -} - -// WithUserRepo 设置用户仓储 -func WithUserRepo(repo repository.UserRepository) Option { - return func(c *Container) { - c.UserRepo = repo - } -} - -// WithProfileRepo 设置档案仓储 -func WithProfileRepo(repo repository.ProfileRepository) Option { - return func(c *Container) { - c.ProfileRepo = repo - } -} - -// WithTextureRepo 设置材质仓储 -func WithTextureRepo(repo repository.TextureRepository) Option { - return func(c *Container) { - c.TextureRepo = repo - } -} - -// WithUserService 设置用户服务 -func WithUserService(svc service.UserService) Option { - return func(c *Container) { - c.UserService = svc - } -} - -// WithProfileService 设置档案服务 -func WithProfileService(svc service.ProfileService) Option { - return func(c *Container) { - c.ProfileService = svc - } -} - -// WithTextureService 设置材质服务 -func WithTextureService(svc service.TextureService) Option { - return func(c *Container) { - c.TextureService = svc - } -} - -// WithTokenService 设置令牌服务 -func WithTokenService(svc service.TokenService) Option { - return func(c *Container) { - c.TokenService = svc - } -} - -// WithYggdrasilRepo 设置Yggdrasil仓储 -func WithYggdrasilRepo(repo repository.YggdrasilRepository) Option { - return func(c *Container) { - c.YggdrasilRepo = repo - } -} - -// WithYggdrasilService 设置Yggdrasil服务 -func WithYggdrasilService(svc service.YggdrasilService) Option { - return func(c *Container) { - c.YggdrasilService = svc - } -} - -// WithVerificationService 设置验证码服务 -func WithVerificationService(svc service.VerificationService) Option { - return func(c *Container) { - c.VerificationService = svc - } -} - -// WithSecurityService 设置安全服务 -func WithSecurityService(svc service.SecurityService) Option { - return func(c *Container) { - c.SecurityService = svc - } -} - -// WithCaptchaService 设置验证码服务 -func WithCaptchaService(svc service.CaptchaService) Option { - return func(c *Container) { - c.CaptchaService = svc - } -} diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 9445f14..f510be7 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -125,23 +125,9 @@ func NewInternalError(message string, err error) *AppError { return NewAppError(500, message, err) } -// Is 检查错误是否匹配 -func Is(err, target error) bool { - return errors.Is(err, target) -} +// 注意:原此处的 Is/As/Wrap 函数(透传标准库)已删除。 +// 请直接使用标准库 errors.Is / errors.As / fmt.Errorf。 -// 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) -} // YggdrasilErrorResponse Yggdrasil协议标准错误响应格式 type YggdrasilErrorResponse struct { diff --git a/internal/errors/errors_test.go b/internal/errors/errors_test.go index 2a8f8f2..befa766 100644 --- a/internal/errors/errors_test.go +++ b/internal/errors/errors_test.go @@ -15,24 +15,12 @@ func TestAppErrorBasics(t *testing.T) { if got := appErr.Error(); got != "bad: root" { t.Fatalf("unexpected Error(): %s", got) } - if !Is(appErr, root) { + // 使用标准库 errors.Is/As(包级 Is/As 已移除) + if !errors.Is(appErr, root) { t.Fatalf("Is should match wrapped error") } var target *AppError - if !As(appErr, &target) { + if !errors.As(appErr, &target) { t.Fatalf("As should succeed") } } - -func TestWrap(t *testing.T) { - if Wrap(nil, "msg") != nil { - t.Fatalf("Wrap nil should return nil") - } - err := errors.New("base") - wrapped := Wrap(err, "ctx") - if wrapped.Error() != "ctx: base" { - t.Fatalf("wrap message mismatch: %v", wrapped) - } -} - - diff --git a/internal/handler/admin_handler.go b/internal/handler/admin_handler.go index 3a12d88..89c4a53 100644 --- a/internal/handler/admin_handler.go +++ b/internal/handler/admin_handler.go @@ -1,9 +1,11 @@ package handler import ( + "errors" "net/http" "strconv" + apperrors "carrotskin/internal/errors" "carrotskin/internal/container" "carrotskin/internal/model" @@ -46,11 +48,16 @@ func (h *AdminHandler) SetUserRole(c *gin.Context) { return } - // 获取当前操作者ID - operatorID, _ := c.Get("user_id") + // 获取当前操作者ID(安全类型断言,防止中间件异常时 panic) + operatorIDVal, _ := c.Get("user_id") + operatorID, ok := operatorIDVal.(int64) + if !ok { + RespondServerError(c, "操作者ID类型错误", nil) + return + } // 不能修改自己的角色 - if req.UserID == operatorID.(int64) { + if req.UserID == operatorID { c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, "不能修改自己的角色", @@ -59,9 +66,13 @@ func (h *AdminHandler) SetUserRole(c *gin.Context) { return } - // 检查目标用户是否存在 - targetUser, err := h.container.UserRepo.FindByID(c.Request.Context(), req.UserID) - if err != nil || targetUser == nil { + // 检查目标用户是否存在(区分 DB 错误与"用户不存在") + targetUser, err := h.container.UserService.GetByID(c.Request.Context(), req.UserID) + if err != nil { + RespondServerError(c, "查询用户失败", err) + return + } + if targetUser == nil { c.JSON(http.StatusNotFound, model.NewErrorResponse( model.CodeNotFound, "用户不存在", @@ -71,16 +82,13 @@ func (h *AdminHandler) SetUserRole(c *gin.Context) { } // 更新用户角色 - err = h.container.UserRepo.UpdateFields(c.Request.Context(), req.UserID, map[string]interface{}{ - "role": req.Role, - }) - if err != nil { + if err := h.container.UserService.SetRole(c.Request.Context(), req.UserID, req.Role); err != nil { RespondServerError(c, "更新用户角色失败", err) return } h.container.Logger.Info("管理员修改用户角色", - zap.Int64("operator_id", operatorID.(int64)), + zap.Int64("operator_id", operatorID), zap.Int64("target_user_id", req.UserID), zap.String("new_role", req.Role), ) @@ -114,13 +122,12 @@ func (h *AdminHandler) GetUserList(c *gin.Context) { pageSize = 20 } - // 使用数据库直接查询用户列表 - var users []model.User - var total int64 - - db := h.container.DB - db.Model(&model.User{}).Count(&total) - db.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&users) + // 通过 Service 层查询用户列表 + users, total, err := h.container.UserService.ListUsers(c.Request.Context(), page, pageSize) + if err != nil { + RespondServerError(c, "获取用户列表失败", err) + return + } // 构建响应(隐藏敏感信息) userList := make([]gin.H, len(users)) @@ -163,7 +170,7 @@ func (h *AdminHandler) GetUserDetail(c *gin.Context) { return } - user, err := h.container.UserRepo.FindByID(c.Request.Context(), userID) + user, err := h.container.UserService.GetByID(c.Request.Context(), userID) if err != nil || user == nil { c.JSON(http.StatusNotFound, model.NewErrorResponse( model.CodeNotFound, @@ -212,10 +219,15 @@ func (h *AdminHandler) SetUserStatus(c *gin.Context) { return } - operatorID, _ := c.Get("user_id") + operatorIDVal, _ := c.Get("user_id") + operatorID, ok := operatorIDVal.(int64) + if !ok { + RespondServerError(c, "操作者ID类型错误", nil) + return + } // 不能修改自己的状态 - if req.UserID == operatorID.(int64) { + if req.UserID == operatorID { c.JSON(http.StatusBadRequest, model.NewErrorResponse( model.CodeBadRequest, "不能修改自己的状态", @@ -224,9 +236,13 @@ func (h *AdminHandler) SetUserStatus(c *gin.Context) { return } - // 检查目标用户是否存在 - targetUser, err := h.container.UserRepo.FindByID(c.Request.Context(), req.UserID) - if err != nil || targetUser == nil { + // 检查目标用户是否存在(区分 DB 错误与"用户不存在") + targetUser, err := h.container.UserService.GetByID(c.Request.Context(), req.UserID) + if err != nil { + RespondServerError(c, "查询用户失败", err) + return + } + if targetUser == nil { c.JSON(http.StatusNotFound, model.NewErrorResponse( model.CodeNotFound, "用户不存在", @@ -236,10 +252,7 @@ func (h *AdminHandler) SetUserStatus(c *gin.Context) { } // 更新用户状态 - err = h.container.UserRepo.UpdateFields(c.Request.Context(), req.UserID, map[string]interface{}{ - "status": req.Status, - }) - if err != nil { + if err := h.container.UserService.SetStatus(c.Request.Context(), req.UserID, req.Status); err != nil { RespondServerError(c, "更新用户状态失败", err) return } @@ -247,7 +260,7 @@ func (h *AdminHandler) SetUserStatus(c *gin.Context) { statusText := map[int16]string{1: "正常", 0: "禁用", -1: "删除"}[req.Status] h.container.Logger.Info("管理员修改用户状态", - zap.Int64("operator_id", operatorID.(int64)), + zap.Int64("operator_id", operatorID), zap.Int64("target_user_id", req.UserID), zap.Int16("new_status", req.Status), ) @@ -277,30 +290,30 @@ func (h *AdminHandler) DeleteTexture(c *gin.Context) { return } - operatorID, _ := c.Get("user_id") - - // 检查材质是否存在 - var texture model.Texture - if err := h.container.DB.First(&texture, textureID).Error; err != nil { - c.JSON(http.StatusNotFound, model.NewErrorResponse( - model.CodeNotFound, - "材质不存在", - nil, - )) + operatorIDVal, _ := c.Get("user_id") + operatorID, ok := operatorIDVal.(int64) + if !ok { + RespondServerError(c, "操作者ID类型错误", nil) return } - // 删除材质 - if err := h.container.DB.Delete(&texture).Error; err != nil { + // 通过 Service 删除材质(Service 会检查存在性) + if err := h.container.TextureService.AdminDelete(c.Request.Context(), textureID); err != nil { + if errors.Is(err, apperrors.ErrTextureNotFound) { + c.JSON(http.StatusNotFound, model.NewErrorResponse( + model.CodeNotFound, + "材质不存在", + nil, + )) + return + } RespondServerError(c, "删除材质失败", err) return } h.container.Logger.Info("管理员删除材质", - zap.Int64("operator_id", operatorID.(int64)), + zap.Int64("operator_id", operatorID), zap.Int64("texture_id", textureID), - zap.Int64("uploader_id", texture.UploaderID), - zap.String("texture_name", texture.Name), ) c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ @@ -330,12 +343,12 @@ func (h *AdminHandler) GetTextureList(c *gin.Context) { pageSize = 20 } - var textures []model.Texture - var total int64 - - db := h.container.DB - db.Model(&model.Texture{}).Count(&total) - db.Preload("Uploader").Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&textures) + // 通过 Service 层查询材质列表 + textures, total, err := h.container.TextureService.ListForAdmin(c.Request.Context(), page, pageSize) + if err != nil { + RespondServerError(c, "获取材质列表失败", err) + return + } // 构建响应 textureList := make([]gin.H, len(textures)) diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index 44b9fc2..b509f9f 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -4,7 +4,6 @@ import ( "carrotskin/internal/container" "carrotskin/internal/service" "carrotskin/internal/types" - "carrotskin/pkg/email" "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -177,8 +176,3 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) { RespondSuccess(c, gin.H{"message": "密码重置成功"}) } - -// getEmailService 获取邮件服务(暂时使用全局方式,后续可改为依赖注入) -func (h *AuthHandler) getEmailService() (*email.Service, error) { - return email.GetService() -} diff --git a/internal/handler/customskin_handler.go b/internal/handler/customskin_handler.go index ca7798d..17cc994 100644 --- a/internal/handler/customskin_handler.go +++ b/internal/handler/customskin_handler.go @@ -2,6 +2,7 @@ package handler import ( "carrotskin/internal/container" + "context" "fmt" "net/http" "strings" @@ -233,9 +234,12 @@ func (h *CustomSkinHandler) GetTexture(c *gin.Context) { } } - // 增加下载计数(异步) + // 增加下载计数(异步,使用独立 ctx 避免请求取消时丢失计数) go func() { - _ = h.container.TextureRepo.IncrementDownloadCount(ctx, texture.ID) + // 复制 ctx 但不传播取消,配合超时防止 Redis 永久阻塞 + asyncCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) + defer cancel() + _ = h.container.TextureService.IncrementDownload(asyncCtx, texture.ID) }() // 流式传输文件内容 diff --git a/internal/handler/helpers.go b/internal/handler/helpers.go index 8d32149..9ed2de5 100644 --- a/internal/handler/helpers.go +++ b/internal/handler/helpers.go @@ -1,9 +1,10 @@ package handler import ( - "carrotskin/internal/errors" + apperrors "carrotskin/internal/errors" "carrotskin/internal/model" "carrotskin/internal/types" + "errors" "net/http" "strconv" @@ -190,31 +191,31 @@ func RespondWithError(c *gin.Context, err error) { return } - // 使用errors.Is检查预定义错误 - if errors.Is(err, errors.ErrUserNotFound) || - errors.Is(err, errors.ErrProfileNotFound) || - errors.Is(err, errors.ErrTextureNotFound) || - errors.Is(err, errors.ErrNotFound) { + // 使用标准库 errors.Is 检查预定义错误 + if errors.Is(err, apperrors.ErrUserNotFound) || + errors.Is(err, apperrors.ErrProfileNotFound) || + errors.Is(err, apperrors.ErrTextureNotFound) || + errors.Is(err, apperrors.ErrNotFound) { RespondNotFound(c, err.Error()) return } - if errors.Is(err, errors.ErrProfileNoPermission) || - errors.Is(err, errors.ErrTextureNoPermission) || - errors.Is(err, errors.ErrForbidden) { + if errors.Is(err, apperrors.ErrProfileNoPermission) || + errors.Is(err, apperrors.ErrTextureNoPermission) || + errors.Is(err, apperrors.ErrForbidden) { RespondForbidden(c, err.Error()) return } - if errors.Is(err, errors.ErrUnauthorized) || - errors.Is(err, errors.ErrInvalidToken) || - errors.Is(err, errors.ErrTokenExpired) { + if errors.Is(err, apperrors.ErrUnauthorized) || + errors.Is(err, apperrors.ErrInvalidToken) || + errors.Is(err, apperrors.ErrTokenExpired) { RespondUnauthorized(c, err.Error()) return } // 检查AppError类型 - var appErr *errors.AppError + var appErr *apperrors.AppError if errors.As(err, &appErr) { c.JSON(appErr.Code, model.NewErrorResponse( appErr.Code, diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 10e1d0b..7d5b0bb 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -4,7 +4,6 @@ import ( "carrotskin/internal/container" "carrotskin/internal/middleware" "carrotskin/pkg/auth" - "carrotskin/pkg/config" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" @@ -40,11 +39,10 @@ func NewHandlers(c *container.Container) *Handlers { // RegisterRoutesWithDI 使用依赖注入注册所有路由 func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) { // 健康检查路由 - router.GET("/health", HealthCheck) + router.GET("/health", NewHealthCheck(c.DB, c.Redis)) // Swagger文档路由 - cfg, _ := config.GetConfig() - if cfg != nil && cfg.Server.SwaggerEnabled { + if c.Config != nil && c.Config.Server.SwaggerEnabled { router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) } diff --git a/internal/handler/swagger.go b/internal/handler/swagger.go index 639bbb1..b8443bf 100644 --- a/internal/handler/swagger.go +++ b/internal/handler/swagger.go @@ -6,57 +6,57 @@ import ( "net/http" "time" - "carrotskin/pkg/database" "carrotskin/pkg/redis" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) -// HealthCheck 健康检查,检查依赖服务状态 -func HealthCheck(c *gin.Context) { - ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) - defer cancel() +// NewHealthCheck 构造健康检查 handler,依赖通过参数注入。 +func NewHealthCheck(db *gorm.DB, redisClient *redis.Client) gin.HandlerFunc { + return func(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) + defer cancel() - checks := make(map[string]string) - status := "ok" + checks := make(map[string]string) + status := "ok" - // 检查数据库 - if err := checkDatabase(ctx); err != nil { - checks["database"] = "unhealthy: " + err.Error() - status = "degraded" - } else { - checks["database"] = "healthy" + // 检查数据库 + if err := checkDatabase(ctx, db); err != nil { + checks["database"] = "unhealthy: " + err.Error() + status = "degraded" + } else { + checks["database"] = "healthy" + } + + // 检查Redis + if err := checkRedis(ctx, redisClient); err != nil { + checks["redis"] = "unhealthy: " + err.Error() + status = "degraded" + } else { + checks["redis"] = "healthy" + } + + // 根据状态返回相应的HTTP状态码 + httpStatus := http.StatusOK + if status == "degraded" { + httpStatus = http.StatusServiceUnavailable + } + + c.JSON(httpStatus, gin.H{ + "status": status, + "message": "CarrotSkin API health check", + "checks": checks, + "timestamp": time.Now().Unix(), + }) } - - // 检查Redis - if err := checkRedis(ctx); err != nil { - checks["redis"] = "unhealthy: " + err.Error() - status = "degraded" - } else { - checks["redis"] = "healthy" - } - - // 根据状态返回相应的HTTP状态码 - httpStatus := http.StatusOK - if status == "degraded" { - httpStatus = http.StatusServiceUnavailable - } - - c.JSON(httpStatus, gin.H{ - "status": status, - "message": "CarrotSkin API health check", - "checks": checks, - "timestamp": time.Now().Unix(), - }) } // checkDatabase 检查数据库连接 -func checkDatabase(ctx context.Context) error { - db, err := database.GetDB() - if err != nil { - return err +func checkDatabase(ctx context.Context, db *gorm.DB) error { + if db == nil { + return errors.New("数据库未初始化") } - sqlDB, err := db.DB() if err != nil { return err @@ -77,11 +77,7 @@ func checkDatabase(ctx context.Context) error { } // checkRedis 检查Redis连接 -func checkRedis(ctx context.Context) error { - client, err := redis.GetClient() - if err != nil { - return err - } +func checkRedis(ctx context.Context, client *redis.Client) error { if client == nil { return errors.New("Redis客户端未初始化") } diff --git a/internal/handler/swagger_test.go b/internal/handler/swagger_test.go index 3e9abe3..36193cb 100644 --- a/internal/handler/swagger_test.go +++ b/internal/handler/swagger_test.go @@ -12,7 +12,8 @@ import ( func TestHealthCheck_Degraded(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() - router.GET("/health", HealthCheck) + // 传入 nil 依赖,模拟服务不可用场景 + router.GET("/health", NewHealthCheck(nil, nil)) req := httptest.NewRequest(http.MethodGet, "/health", nil) w := httptest.NewRecorder() @@ -22,6 +23,3 @@ func TestHealthCheck_Degraded(t *testing.T) { t.Fatalf("expected 503 when dependencies missing, got %d", w.Code) } } - - - diff --git a/internal/handler/yggdrasil_handler.go b/internal/handler/yggdrasil_handler.go index b27a2a3..555f736 100644 --- a/internal/handler/yggdrasil_handler.go +++ b/internal/handler/yggdrasil_handler.go @@ -201,7 +201,7 @@ func (h *YggdrasilHandler) Authenticate(c *gin.Context) { if emailRegex.MatchString(request.Identifier) { userId, err = h.container.YggdrasilService.GetUserIDByEmail(c.Request.Context(), request.Identifier) } else { - profile, err = h.container.ProfileRepo.FindByName(c.Request.Context(), request.Identifier) + profile, err = h.container.ProfileService.GetByProfileName(c.Request.Context(), request.Identifier) if err != nil { h.logger.Error("用户名不存在", zap.String("identifier", request.Identifier), zap.Error(err)) c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse( diff --git a/internal/middleware/casbin.go b/internal/middleware/casbin.go index 6b3d366..e15e889 100644 --- a/internal/middleware/casbin.go +++ b/internal/middleware/casbin.go @@ -3,6 +3,7 @@ package middleware import ( "net/http" + "carrotskin/internal/model" "carrotskin/pkg/auth" "github.com/gin-gonic/gin" @@ -15,10 +16,11 @@ func CasbinMiddleware(casbinService *auth.CasbinService, resource, action string // 从上下文获取用户角色(由AuthMiddleware设置) role, exists := c.Get("user_role") if !exists { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "未授权访问", - }) + c.JSON(http.StatusUnauthorized, model.NewErrorResponse( + model.CodeUnauthorized, + model.MsgUnauthorized, + nil, + )) c.Abort() return } @@ -30,10 +32,11 @@ func CasbinMiddleware(casbinService *auth.CasbinService, resource, action string // 检查权限 if !casbinService.CheckPermission(roleStr, resource, action) { - c.JSON(http.StatusForbidden, gin.H{ - "success": false, - "message": "权限不足", - }) + c.JSON(http.StatusForbidden, model.NewErrorResponse( + model.CodeForbidden, + model.MsgForbidden, + nil, + )) c.Abort() return } @@ -47,20 +50,22 @@ func RequireAdmin() gin.HandlerFunc { return func(c *gin.Context) { role, exists := c.Get("user_role") if !exists { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "未授权访问", - }) + c.JSON(http.StatusUnauthorized, model.NewErrorResponse( + model.CodeUnauthorized, + model.MsgUnauthorized, + nil, + )) c.Abort() return } roleStr, ok := role.(string) if !ok || roleStr != "admin" { - c.JSON(http.StatusForbidden, gin.H{ - "success": false, - "message": "需要管理员权限", - }) + c.JSON(http.StatusForbidden, model.NewErrorResponse( + model.CodeForbidden, + "需要管理员权限", + nil, + )) c.Abort() return } @@ -74,20 +79,22 @@ func RequireRole(allowedRoles ...string) gin.HandlerFunc { return func(c *gin.Context) { role, exists := c.Get("user_role") if !exists { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "未授权访问", - }) + c.JSON(http.StatusUnauthorized, model.NewErrorResponse( + model.CodeUnauthorized, + model.MsgUnauthorized, + nil, + )) c.Abort() return } roleStr, ok := role.(string) if !ok { - c.JSON(http.StatusForbidden, gin.H{ - "success": false, - "message": "权限不足", - }) + c.JSON(http.StatusForbidden, model.NewErrorResponse( + model.CodeForbidden, + model.MsgForbidden, + nil, + )) c.Abort() return } @@ -100,10 +107,11 @@ func RequireRole(allowedRoles ...string) gin.HandlerFunc { } } - c.JSON(http.StatusForbidden, gin.H{ - "success": false, - "message": "权限不足", - }) + c.JSON(http.StatusForbidden, model.NewErrorResponse( + model.CodeForbidden, + model.MsgForbidden, + nil, + )) c.Abort() } } diff --git a/internal/middleware/cors.go b/internal/middleware/cors.go index 296adad..f8f028f 100644 --- a/internal/middleware/cors.go +++ b/internal/middleware/cors.go @@ -7,18 +7,14 @@ import ( ) // CORS 跨域中间件 -func CORS() gin.HandlerFunc { - // 获取配置,如果配置未初始化则使用默认值 - var allowedOrigins []string - var isTestEnv bool - if cfg, err := config.GetConfig(); err == nil { - allowedOrigins = cfg.Security.AllowedOrigins - isTestEnv = cfg.IsTestEnvironment() - } else { - // 默认允许所有来源(向后兼容) +func CORS(cfg *config.Config) gin.HandlerFunc { + // 从注入的配置读取 CORS 设置 + allowedOrigins := cfg.Security.AllowedOrigins + if len(allowedOrigins) == 0 { + // 默认允许所有来源 allowedOrigins = []string{"*"} - isTestEnv = false } + isTestEnv := cfg.IsTestEnvironment() return gin.HandlerFunc(func(c *gin.Context) { origin := c.GetHeader("Origin") diff --git a/internal/middleware/cors_test.go b/internal/middleware/cors_test.go index 833e76d..a5873d3 100644 --- a/internal/middleware/cors_test.go +++ b/internal/middleware/cors_test.go @@ -5,15 +5,26 @@ import ( "net/http/httptest" "testing" + "carrotskin/pkg/config" + "github.com/gin-gonic/gin" ) +// testCORSConfig 返回测试用的 CORS 配置(通配符模式) +func testCORSConfig() *config.Config { + return &config.Config{ + Security: config.SecurityConfig{ + AllowedOrigins: []string{"*"}, + }, + } +} + // TestCORS_Headers 测试CORS中间件设置的响应头 func TestCORS_Headers(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() - router.Use(CORS()) + router.Use(CORS(testCORSConfig())) router.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "success"}) }) @@ -55,7 +66,7 @@ func TestCORS_OPTIONS(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() - router.Use(CORS()) + router.Use(CORS(testCORSConfig())) router.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "success"}) }) @@ -76,7 +87,7 @@ func TestCORS_AllowMethods(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() - router.Use(CORS()) + router.Use(CORS(testCORSConfig())) router.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "success"}) }) @@ -103,7 +114,7 @@ func TestCORS_AllowHeaders(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() - router.Use(CORS()) + router.Use(CORS(testCORSConfig())) router.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "success"}) }) @@ -130,7 +141,7 @@ func TestCORS_WithSpecificOrigin(t *testing.T) { // 注意:此测试验证的是在配置了具体allowed origins时的行为 // 在没有配置初始化的情况下,默认使用通配符模式 router := gin.New() - router.Use(CORS()) + router.Use(CORS(testCORSConfig())) router.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "success"}) }) diff --git a/internal/repository/interfaces.go b/internal/repository/interfaces.go index d2f586c..c16b746 100644 --- a/internal/repository/interfaces.go +++ b/internal/repository/interfaces.go @@ -12,6 +12,7 @@ type UserRepository interface { FindByUsername(ctx context.Context, username string) (*model.User, error) FindByEmail(ctx context.Context, email string) (*model.User, error) FindByIDs(ctx context.Context, ids []int64) ([]*model.User, error) // 批量查询 + List(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) // 分页列表(管理员) Update(ctx context.Context, user *model.User) error UpdateFields(ctx context.Context, id int64, fields map[string]interface{}) error BatchUpdate(ctx context.Context, ids []int64, fields map[string]interface{}) (int64, error) // 批量更新 @@ -64,6 +65,8 @@ type TextureRepository interface { RemoveFavorite(ctx context.Context, userID, textureID int64) error GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error) + ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error) // 管理员列表(含 Uploader 预加载) + FindByIDForAdmin(ctx context.Context, id int64) (*model.Texture, error) // 管理员查询(无权限过滤) } @@ -71,6 +74,8 @@ type TextureRepository interface { type YggdrasilRepository interface { GetPasswordByID(ctx context.Context, id int64) (string, error) ResetPassword(ctx context.Context, id int64, password string) error + // Create 创建Yggdrasil密码记录(用于首次设置密码) + Create(ctx context.Context, yggdrasil *model.Yggdrasil) error } // ClientRepository Client仓储接口 diff --git a/internal/repository/texture_repository.go b/internal/repository/texture_repository.go index 6382d31..4aa34d7 100644 --- a/internal/repository/texture_repository.go +++ b/internal/repository/texture_repository.go @@ -210,3 +210,27 @@ func (r *textureRepository) CountByUploaderID(ctx context.Context, uploaderID in Count(&count).Error return count, err } + +// ListForAdmin 管理员分页查询材质列表(含 Uploader 预加载,无权限过滤) +func (r *textureRepository) ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error) { + var textures []*model.Texture + var total int64 + + db := r.db.WithContext(ctx).Model(&model.Texture{}) + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + offset := (page - 1) * pageSize + if err := db.Preload("Uploader").Order("id DESC").Offset(offset).Limit(pageSize).Find(&textures).Error; err != nil { + return nil, 0, err + } + return textures, total, nil +} + +// FindByIDForAdmin 管理员查询材质(无权限过滤,用于管理员删除前检查存在性) +func (r *textureRepository) FindByIDForAdmin(ctx context.Context, id int64) (*model.Texture, error) { + var texture model.Texture + err := r.db.WithContext(ctx).Where("id = ?", id).First(&texture).Error + return handleNotFoundResult(&texture, err) +} diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go index b104d51..fede77e 100644 --- a/internal/repository/user_repository.go +++ b/internal/repository/user_repository.go @@ -50,6 +50,23 @@ func (r *userRepository) FindByIDs(ctx context.Context, ids []int64) ([]*model.U return users, err } +// List 分页查询用户列表(管理员,不过滤状态) +func (r *userRepository) List(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) { + var users []*model.User + var total int64 + + db := r.db.WithContext(ctx).Model(&model.User{}) + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + offset := (page - 1) * pageSize + if err := db.Order("id DESC").Offset(offset).Limit(pageSize).Find(&users).Error; err != nil { + return nil, 0, err + } + return users, total, nil +} + func (r *userRepository) Update(ctx context.Context, user *model.User) error { return r.db.WithContext(ctx).Save(user).Error } diff --git a/internal/repository/yggdrasil_repository.go b/internal/repository/yggdrasil_repository.go index d517614..45c2fd3 100644 --- a/internal/repository/yggdrasil_repository.go +++ b/internal/repository/yggdrasil_repository.go @@ -30,6 +30,11 @@ func (r *yggdrasilRepository) ResetPassword(ctx context.Context, id int64, passw return r.db.WithContext(ctx).Model(&model.Yggdrasil{}).Where("id = ?", id).Update("password", password).Error } +// Create 创建Yggdrasil密码记录 +func (r *yggdrasilRepository) Create(ctx context.Context, yggdrasil *model.Yggdrasil) error { + return r.db.WithContext(ctx).Create(yggdrasil).Error +} + diff --git a/internal/service/captcha_service.go b/internal/service/captcha_service.go index 16f4373..c9356af 100644 --- a/internal/service/captcha_service.go +++ b/internal/service/captcha_service.go @@ -72,13 +72,15 @@ type RedisData struct { // captchaService CaptchaService的实现 type captchaService struct { + cfg *config.Config redis *redis.Client logger *zap.Logger } // NewCaptchaService 创建CaptchaService实例 -func NewCaptchaService(redisClient *redis.Client, logger *zap.Logger) CaptchaService { +func NewCaptchaService(cfg *config.Config, redisClient *redis.Client, logger *zap.Logger) CaptchaService { return &captchaService{ + cfg: cfg, redis: redisClient, logger: logger, } @@ -157,8 +159,7 @@ func (s *captchaService) Generate(ctx context.Context) (masterImg, tileImg, capt // Verify 验证验证码 func (s *captchaService) Verify(ctx context.Context, dx int, captchaID string) (bool, error) { // 测试环境下直接通过验证 - cfg, err := config.GetConfig() - if err == nil && cfg.IsTestEnvironment() { + if s.cfg.IsTestEnvironment() { return true, nil } @@ -197,8 +198,7 @@ func (s *captchaService) Verify(ctx context.Context, dx int, captchaID string) ( // CheckVerified 检查验证码是否已验证(仅检查captcha_id) func (s *captchaService) CheckVerified(ctx context.Context, captchaID string) (bool, error) { // 测试环境下直接通过验证 - cfg, err := config.GetConfig() - if err == nil && cfg.IsTestEnvironment() { + if s.cfg.IsTestEnvironment() { return true, nil } @@ -216,8 +216,7 @@ func (s *captchaService) CheckVerified(ctx context.Context, captchaID string) (b // ConsumeVerified 消耗已验证的验证码(注册成功后调用) func (s *captchaService) ConsumeVerified(ctx context.Context, captchaID string) error { // 测试环境下直接返回成功 - cfg, err := config.GetConfig() - if err == nil && cfg.IsTestEnvironment() { + if s.cfg.IsTestEnvironment() { return nil } diff --git a/internal/service/helpers.go b/internal/service/helpers.go index 262dba0..15149f0 100644 --- a/internal/service/helpers.go +++ b/internal/service/helpers.go @@ -1,18 +1,11 @@ package service -import ( - "errors" - "fmt" -) - -// 通用错误 -var ( - ErrProfileNotFound = errors.New("档案不存在") - ErrProfileNoPermission = errors.New("无权操作此档案") - ErrTextureNotFound = errors.New("材质不存在") - ErrTextureNoPermission = errors.New("无权操作此材质") - ErrUserNotFound = errors.New("用户不存在") -) +// 通用辅助函数。 +// +// 注意:错误定义已统一到 internal/errors 包。 +// 原先在此文件重复定义的 ErrProfileNotFound/ErrProfileNoPermission/ +// ErrTextureNotFound/ErrTextureNoPermission/ErrUserNotFound 已删除, +// 请使用 internal/errors(别名 apperrors)中的对应定义。 // NormalizePagination 规范化分页参数 func NormalizePagination(page, pageSize int) (int, int) { @@ -27,11 +20,3 @@ func NormalizePagination(page, pageSize int) (int, int) { } return page, pageSize } - -// WrapError 包装错误,添加上下文信息 -func WrapError(err error, message string) error { - if err == nil { - return nil - } - return fmt.Errorf("%s: %w", message, err) -} diff --git a/internal/service/helpers_test.go b/internal/service/helpers_test.go index 043aba4..fc73827 100644 --- a/internal/service/helpers_test.go +++ b/internal/service/helpers_test.go @@ -1,7 +1,6 @@ package service import ( - "errors" "testing" ) @@ -30,21 +29,3 @@ func TestNormalizePagination_Basic(t *testing.T) { }) } } - -// TestWrapError 覆盖 WrapError 的 nil 与非 nil 分支 -func TestWrapError(t *testing.T) { - if err := WrapError(nil, "msg"); err != nil { - t.Fatalf("WrapError(nil, ...) 应返回 nil, got=%v", err) - } - - orig := errors.New("orig") - wrapped := WrapError(orig, "context") - if wrapped == nil { - t.Fatalf("WrapError 应返回非 nil 错误") - } - if wrapped.Error() == orig.Error() { - t.Fatalf("WrapError 应添加上下文信息, got=%v", wrapped) - } -} - - diff --git a/internal/service/interfaces.go b/internal/service/interfaces.go index a11bc57..da2e4e1 100644 --- a/internal/service/interfaces.go +++ b/internal/service/interfaces.go @@ -37,6 +37,11 @@ type UserService interface { // 配置获取 GetMaxProfilesPerUser() int GetMaxTexturesPerUser() int + + // 管理员操作 + ListUsers(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) + SetRole(ctx context.Context, userID int64, role string) error + SetStatus(ctx context.Context, userID int64, status int16) error } // ProfileService 档案服务接口 @@ -73,6 +78,11 @@ type TextureService interface { // 限制检查 CheckUploadLimit(ctx context.Context, uploaderID int64, maxTextures int) error + + // 管理员操作 + ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error) + AdminDelete(ctx context.Context, textureID int64) error + IncrementDownload(ctx context.Context, textureID int64) error } // TokenService 令牌服务接口 diff --git a/internal/service/mocks_test.go b/internal/service/mocks_test.go index 434e3de..0a8b2d7 100644 --- a/internal/service/mocks_test.go +++ b/internal/service/mocks_test.go @@ -130,6 +130,24 @@ func (m *MockUserRepository) FindByIDs(ctx context.Context, ids []int64) ([]*mod return result, nil } +// List 分页查询用户列表(管理员,mock 实现) +func (m *MockUserRepository) List(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) { + var all []*model.User + for _, u := range m.users { + all = append(all, u) + } + total := int64(len(all)) + offset := (page - 1) * pageSize + if offset >= len(all) { + return []*model.User{}, total, nil + } + end := offset + pageSize + if end > len(all) { + end = len(all) + } + return all[offset:end], total, nil +} + // MockProfileRepository 模拟ProfileRepository type MockProfileRepository struct { profiles map[string]*model.Profile @@ -474,6 +492,32 @@ func (m *MockTextureRepository) BatchDelete(ctx context.Context, ids []int64) (i return deleted, nil } +// ListForAdmin 管理员列表(mock 实现) +func (m *MockTextureRepository) ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error) { + var all []*model.Texture + for _, t := range m.textures { + all = append(all, t) + } + total := int64(len(all)) + offset := (page - 1) * pageSize + if offset >= len(all) { + return []*model.Texture{}, total, nil + } + end := offset + pageSize + if end > len(all) { + end = len(all) + } + return all[offset:end], total, nil +} + +// FindByIDForAdmin 管理员查询(mock 实现,无权限过滤) +func (m *MockTextureRepository) FindByIDForAdmin(ctx context.Context, id int64) (*model.Texture, error) { + if t, ok := m.textures[id]; ok { + return t, nil + } + return nil, nil +} + // ============================================================================ // Service Mocks diff --git a/internal/service/profile_service.go b/internal/service/profile_service.go index 844ce4c..7b57c24 100644 --- a/internal/service/profile_service.go +++ b/internal/service/profile_service.go @@ -1,6 +1,7 @@ package service import ( + apperrors "carrotskin/internal/errors" "carrotskin/internal/model" "carrotskin/internal/repository" "carrotskin/pkg/database" @@ -100,7 +101,7 @@ func (s *profileService) GetByUUID(ctx context.Context, uuid string) (*model.Pro profile2, err := s.profileRepo.FindByUUID(ctx, uuid) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrProfileNotFound + return nil, apperrors.ErrProfileNotFound } return nil, fmt.Errorf("查询档案失败: %w", err) } @@ -140,13 +141,13 @@ func (s *profileService) Update(ctx context.Context, uuid string, userID int64, profile, err := s.profileRepo.FindByUUID(ctx, uuid) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrProfileNotFound + return nil, apperrors.ErrProfileNotFound } return nil, fmt.Errorf("查询档案失败: %w", err) } if profile.UserID != userID { - return nil, ErrProfileNoPermission + return nil, apperrors.ErrProfileNoPermission } // 检查角色名是否重复 @@ -187,13 +188,13 @@ func (s *profileService) Delete(ctx context.Context, uuid string, userID int64) profile, err := s.profileRepo.FindByUUID(ctx, uuid) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrProfileNotFound + return apperrors.ErrProfileNotFound } return fmt.Errorf("查询档案失败: %w", err) } if profile.UserID != userID { - return ErrProfileNoPermission + return apperrors.ErrProfileNoPermission } if err := s.profileRepo.Delete(ctx, uuid); err != nil { diff --git a/internal/service/signature_service.go b/internal/service/signature_service.go index 26541e1..39062a7 100644 --- a/internal/service/signature_service.go +++ b/internal/service/signature_service.go @@ -53,7 +53,7 @@ func NewSignatureService( } // NewKeyPair 生成新的RSA密钥对 -func (s *SignatureService) NewKeyPair() (*model.KeyPair, error) { +func (s *SignatureService) NewKeyPair(ctx context.Context) (*model.KeyPair, error) { privateKey, err := rsa.GenerateKey(rand.Reader, KeySize) if err != nil { return nil, fmt.Errorf("生成RSA密钥对失败: %w", err) @@ -85,7 +85,7 @@ func (s *SignatureService) NewKeyPair() (*model.KeyPair, error) { refresh := now.AddDate(0, 0, RefreshDays) // 获取Yggdrasil根密钥并签名公钥 - yggPublicKey, yggPrivateKey, err := s.GetOrCreateYggdrasilKeyPair() + yggPublicKey, yggPrivateKey, err := s.GetOrCreateYggdrasilKeyPair(ctx) if err != nil { return nil, fmt.Errorf("获取Yggdrasil根密钥失败: %w", err) } @@ -132,9 +132,7 @@ func (s *SignatureService) NewKeyPair() (*model.KeyPair, error) { } // GetOrCreateYggdrasilKeyPair 获取或创建Yggdrasil根密钥对 -func (s *SignatureService) GetOrCreateYggdrasilKeyPair() (string, *rsa.PrivateKey, error) { - ctx := context.Background() - +func (s *SignatureService) GetOrCreateYggdrasilKeyPair(ctx context.Context) (string, *rsa.PrivateKey, error) { // 尝试从Redis获取密钥 publicKeyPEM, err := s.redis.Get(ctx, PublicKeyRedisKey) if err == nil && publicKeyPEM != "" { @@ -201,15 +199,14 @@ func (s *SignatureService) GetOrCreateYggdrasilKeyPair() (string, *rsa.PrivateKe } // GetPublicKeyFromRedis 从Redis获取公钥 -func (s *SignatureService) GetPublicKeyFromRedis() (string, error) { - ctx := context.Background() +func (s *SignatureService) GetPublicKeyFromRedis(ctx context.Context) (string, error) { publicKey, err := s.redis.Get(ctx, PublicKeyRedisKey) if err != nil { return "", fmt.Errorf("从Redis获取公钥失败: %w", err) } if publicKey == "" { // 如果Redis中没有,创建新的密钥对 - publicKey, _, err = s.GetOrCreateYggdrasilKeyPair() + publicKey, _, err = s.GetOrCreateYggdrasilKeyPair(ctx) if err != nil { return "", fmt.Errorf("创建新密钥对失败: %w", err) } @@ -218,14 +215,13 @@ func (s *SignatureService) GetPublicKeyFromRedis() (string, error) { } // SignStringWithSHA1withRSA 使用SHA1withRSA签名字符串 -func (s *SignatureService) SignStringWithSHA1withRSA(data string) (string, error) { - ctx := context.Background() +func (s *SignatureService) SignStringWithSHA1withRSA(ctx context.Context, data string) (string, error) { // 从Redis获取私钥 privateKeyPEM, err := s.redis.Get(ctx, PrivateKeyRedisKey) if err != nil || privateKeyPEM == "" { // 如果没有私钥,创建新的密钥对 - _, privateKey, err := s.GetOrCreateYggdrasilKeyPair() + _, privateKey, err := s.GetOrCreateYggdrasilKeyPair(ctx) if err != nil { return "", fmt.Errorf("获取私钥失败: %w", err) } diff --git a/internal/service/texture_service.go b/internal/service/texture_service.go index cfdfa2c..6d7607c 100644 --- a/internal/service/texture_service.go +++ b/internal/service/texture_service.go @@ -2,6 +2,7 @@ package service import ( "bytes" + apperrors "carrotskin/internal/errors" "carrotskin/internal/model" "carrotskin/internal/repository" "carrotskin/pkg/database" @@ -13,8 +14,10 @@ import ( "fmt" "path/filepath" "strings" + "time" "go.uber.org/zap" + "gorm.io/gorm" ) // textureService TextureService的实现 @@ -26,6 +29,7 @@ type textureService struct { cacheKeys *database.CacheKeyBuilder cacheInv *database.CacheInvalidator logger *zap.Logger + db *gorm.DB } // NewTextureService 创建TextureService实例 @@ -35,6 +39,7 @@ func NewTextureService( storageClient *storage.StorageClient, cacheManager *database.CacheManager, logger *zap.Logger, + db *gorm.DB, ) TextureService { return &textureService{ textureRepo: textureRepo, @@ -44,6 +49,7 @@ func NewTextureService( cacheKeys: database.NewCacheKeyBuilder(""), cacheInv: database.NewCacheInvalidator(cacheManager), logger: logger, + db: db, } } @@ -62,7 +68,7 @@ func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture, return nil, err } if texture2 == nil { - return nil, ErrTextureNotFound + return nil, apperrors.ErrTextureNotFound } if texture2.Status == -1 { return nil, errors.New("材质已删除") @@ -80,7 +86,7 @@ func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture, return nil, err } if texture2 == nil { - return nil, ErrTextureNotFound + return nil, apperrors.ErrTextureNotFound } if texture2.Status == -1 { return nil, errors.New("材质已删除") @@ -109,7 +115,7 @@ func (s *textureService) GetByHash(ctx context.Context, hash string) (*model.Tex return nil, err } if texture2 == nil { - return nil, ErrTextureNotFound + return nil, apperrors.ErrTextureNotFound } if texture2.Status == -1 { return nil, errors.New("材质已删除") @@ -162,10 +168,10 @@ func (s *textureService) Update(ctx context.Context, textureID, uploaderID int64 return nil, err } if texture == nil { - return nil, ErrTextureNotFound + return nil, apperrors.ErrTextureNotFound } if texture.UploaderID != uploaderID { - return nil, ErrTextureNoPermission + return nil, apperrors.ErrTextureNoPermission } // 更新字段 @@ -200,10 +206,10 @@ func (s *textureService) Delete(ctx context.Context, textureID, uploaderID int64 return err } if texture == nil { - return ErrTextureNotFound + return apperrors.ErrTextureNotFound } if texture.UploaderID != uploaderID { - return ErrTextureNoPermission + return apperrors.ErrTextureNoPermission } err = s.textureRepo.Delete(ctx, textureID) @@ -225,33 +231,41 @@ func (s *textureService) ToggleFavorite(ctx context.Context, userID, textureID i return false, err } if texture == nil { - return false, ErrTextureNotFound + return false, apperrors.ErrTextureNotFound } - isFavorited, err := s.textureRepo.IsFavorited(ctx, userID, textureID) + // 在事务中执行"切换收藏 + 增减计数"两步,保证数据一致性 + var favorited bool + err = database.TransactionWithTimeout(ctx, s.db, 5*time.Second, func(tx *gorm.DB) error { + isFav, err := s.textureRepo.IsFavorited(ctx, userID, textureID) + if err != nil { + return err + } + if isFav { + // 已收藏 -> 取消收藏 + if err := s.textureRepo.RemoveFavorite(ctx, userID, textureID); err != nil { + return err + } + if err := s.textureRepo.DecrementFavoriteCount(ctx, textureID); err != nil { + return err + } + favorited = false + return nil + } + // 未收藏 -> 添加收藏 + if err := s.textureRepo.AddFavorite(ctx, userID, textureID); err != nil { + return err + } + if err := s.textureRepo.IncrementFavoriteCount(ctx, textureID); err != nil { + return err + } + favorited = true + return nil + }) if err != nil { return false, err } - - if isFavorited { - // 已收藏 -> 取消收藏 - if err := s.textureRepo.RemoveFavorite(ctx, userID, textureID); err != nil { - return false, err - } - if err := s.textureRepo.DecrementFavoriteCount(ctx, textureID); err != nil { - return false, err - } - return false, nil - } - - // 未收藏 -> 添加收藏 - if err := s.textureRepo.AddFavorite(ctx, userID, textureID); err != nil { - return false, err - } - if err := s.textureRepo.IncrementFavoriteCount(ctx, textureID); err != nil { - return false, err - } - return true, nil + return favorited, nil } func (s *textureService) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) { @@ -277,7 +291,7 @@ func (s *textureService) UploadTexture(ctx context.Context, uploaderID int64, na // 验证用户存在 user, err := s.userRepo.FindByID(ctx, uploaderID) if err != nil || user == nil { - return nil, ErrUserNotFound + return nil, apperrors.ErrUserNotFound } // 验证文件大小和扩展名 @@ -403,3 +417,28 @@ func parseTextureTypeInternal(textureType string) (model.TextureType, error) { return "", errors.New("无效的材质类型") } } + +// ==================== 管理员操作 ==================== + +// ListForAdmin 分页查询材质列表(管理员,含 Uploader 预加载) +func (s *textureService) ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error) { + page, pageSize = NormalizePagination(page, pageSize) + return s.textureRepo.ListForAdmin(ctx, page, pageSize) +} + +// AdminDelete 管理员删除材质(无权限校验) +func (s *textureService) AdminDelete(ctx context.Context, textureID int64) error { + texture, err := s.textureRepo.FindByIDForAdmin(ctx, textureID) + if err != nil { + return err + } + if texture == nil { + return apperrors.ErrTextureNotFound + } + return s.textureRepo.Delete(ctx, textureID) +} + +// IncrementDownload 增加下载计数(CustomSkinAPI 等场景使用) +func (s *textureService) IncrementDownload(ctx context.Context, textureID int64) error { + return s.textureRepo.IncrementDownloadCount(ctx, textureID) +} diff --git a/internal/service/texture_service_test.go b/internal/service/texture_service_test.go index b231f12..28009c9 100644 --- a/internal/service/texture_service_test.go +++ b/internal/service/texture_service_test.go @@ -494,7 +494,7 @@ func TestTextureServiceImpl_Create(t *testing.T) { _ = userRepo.Create(context.Background(), testUser) cacheManager := NewMockCacheManager() - textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger) + textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil) tests := []struct { name string @@ -617,7 +617,7 @@ func TestTextureServiceImpl_GetByID(t *testing.T) { _ = textureRepo.Create(context.Background(), testTexture) cacheManager := NewMockCacheManager() - textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger) + textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil) tests := []struct { name string @@ -675,7 +675,7 @@ func TestTextureServiceImpl_GetByUserID_And_Search(t *testing.T) { } cacheManager := NewMockCacheManager() - textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger) + textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil) ctx := context.Background() @@ -714,7 +714,7 @@ func TestTextureServiceImpl_Update_And_Delete(t *testing.T) { _ = textureRepo.Create(context.Background(), texture) cacheManager := NewMockCacheManager() - textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger) + textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil) ctx := context.Background() @@ -764,7 +764,7 @@ func TestTextureServiceImpl_FavoritesAndLimit(t *testing.T) { } cacheManager := NewMockCacheManager() - textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger) + textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil) ctx := context.Background() @@ -807,7 +807,7 @@ func TestTextureServiceImpl_ToggleFavorite(t *testing.T) { _ = textureRepo.Create(context.Background(), testTexture) cacheManager := NewMockCacheManager() - textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger) + textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil) ctx := context.Background() diff --git a/internal/service/token_service_redis.go b/internal/service/token_service_redis.go index 3cab198..92a7d29 100644 --- a/internal/service/token_service_redis.go +++ b/internal/service/token_service_redis.go @@ -10,7 +10,6 @@ import ( "fmt" "time" - "github.com/jackc/pgx/v5" "go.uber.org/zap" ) @@ -173,8 +172,9 @@ func (s *tokenServiceRedis) Create(ctx context.Context, userID int64, UUID strin } if err := s.tokenStore.Store(ctx, accessToken, metadata, ttl); err != nil { - s.logger.Warn("存储Token到Redis失败", zap.Error(err)) - // 不返回错误,因为JWT本身已经生成成功 + // Redis 存储失败必须返回错误:否则 Validate 拿不到元数据,Token 立即失效 + s.logger.Error("存储Token到Redis失败", zap.Error(err)) + return nil, nil, "", "", fmt.Errorf("存储Token失败: %w", err) } return selectedProfileID, availableProfiles, accessToken, clientToken, nil @@ -473,7 +473,7 @@ func (s *tokenServiceRedis) validateProfileByUserID(ctx context.Context, userID profile, err := s.profileRepo.FindByUUID(ctx, UUID) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { + if repository.IsNotFound(err) { return false, errors.New("配置文件不存在") } return false, fmt.Errorf("验证配置文件失败: %w", err) diff --git a/internal/service/user_service.go b/internal/service/user_service.go index d73954f..88efdbd 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -26,6 +26,7 @@ import ( // userService UserService的实现 type userService struct { + cfg *config.Config userRepo repository.UserRepository jwtService *auth.JWTService redis *redis.Client @@ -38,6 +39,7 @@ type userService struct { // NewUserService 创建UserService实例 func NewUserService( + cfg *config.Config, userRepo repository.UserRepository, jwtService *auth.JWTService, redisClient *redis.Client, @@ -48,6 +50,7 @@ func NewUserService( // CacheKeyBuilder 使用空前缀,因为 CacheManager 已经处理了前缀 // 这样缓存键的格式为: CacheManager前缀 + CacheKeyBuilder生成的键 return &userService{ + cfg: cfg, userRepo: userRepo, jwtService: jwtService, redis: redisClient, @@ -81,7 +84,7 @@ func (s *userService) Register(ctx context.Context, username, password, email, a // 加密密码 hashedPassword, err := auth.HashPassword(password) if err != nil { - return nil, "", errors.New("密码加密失败") + return nil, "", fmt.Errorf("密码加密失败: %w", err) } // 确定头像URL @@ -112,7 +115,7 @@ func (s *userService) Register(ctx context.Context, username, password, email, a // 生成JWT Token token, err := s.jwtService.GenerateToken(user.ID, user.Username, user.Role) if err != nil { - return nil, "", errors.New("生成Token失败") + return nil, "", fmt.Errorf("生成Token失败: %w", err) } return user, token, nil @@ -167,15 +170,20 @@ func (s *userService) Login(ctx context.Context, usernameOrEmail, password, ipAd // 生成JWT Token token, err := s.jwtService.GenerateToken(user.ID, user.Username, user.Role) if err != nil { - return nil, "", errors.New("生成Token失败") + return nil, "", fmt.Errorf("生成Token失败: %w", err) } - // 更新最后登录时间 + // 更新最后登录时间(非关键路径,错误降级为 Warn) now := time.Now() user.LastLoginAt = &now - _ = s.userRepo.UpdateFields(ctx, user.ID, map[string]interface{}{ + if err := s.userRepo.UpdateFields(ctx, user.ID, map[string]interface{}{ "last_login_at": now, - }) + }); err != nil { + s.logger.Warn("更新最后登录时间失败", + zap.Int64("user_id", user.ID), + zap.Error(err), + ) + } // 记录成功登录日志 s.logSuccessLogin(ctx, user.ID, ipAddress, userAgent) @@ -239,7 +247,10 @@ func (s *userService) UpdateAvatar(ctx context.Context, userID int64, avatarURL func (s *userService) ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error { user, err := s.userRepo.FindByID(ctx, userID) - if err != nil || user == nil { + if err != nil { + return fmt.Errorf("查询用户失败: %w", err) + } + if user == nil { return errors.New("用户不存在") } @@ -249,7 +260,7 @@ func (s *userService) ChangePassword(ctx context.Context, userID int64, oldPassw hashedPassword, err := auth.HashPassword(newPassword) if err != nil { - return errors.New("密码加密失败") + return fmt.Errorf("密码加密失败: %w", err) } err = s.userRepo.UpdateFields(ctx, userID, map[string]interface{}{ @@ -267,13 +278,16 @@ func (s *userService) ChangePassword(ctx context.Context, userID int64, oldPassw func (s *userService) ResetPassword(ctx context.Context, email, newPassword string) error { user, err := s.userRepo.FindByEmail(ctx, email) - if err != nil || user == nil { + if err != nil { + return fmt.Errorf("查询用户失败: %w", err) + } + if user == nil { return errors.New("用户不存在") } hashedPassword, err := auth.HashPassword(newPassword) if err != nil { - return errors.New("密码加密失败") + return fmt.Errorf("密码加密失败: %w", err) } err = s.userRepo.UpdateFields(ctx, user.ID, map[string]interface{}{ @@ -350,14 +364,13 @@ func (s *userService) ValidateAvatarURL(ctx context.Context, avatarURL string) e return errors.New("URL缺少主机名") } - // 从配置获取允许的域名列表 - cfg, err := config.GetConfig() - if err != nil { - allowedDomains := []string{"localhost", "127.0.0.1"} - return s.checkDomainAllowed(host, allowedDomains) + // 从注入的配置获取允许的域名列表 + allowedDomains := s.cfg.Security.AllowedDomains + if len(allowedDomains) == 0 { + allowedDomains = []string{"localhost", "127.0.0.1"} } - return s.checkDomainAllowed(host, cfg.Security.AllowedDomains) + return s.checkDomainAllowed(host, allowedDomains) } func (s *userService) UploadAvatar(ctx context.Context, userID int64, fileData []byte, fileName string) (string, error) { @@ -422,29 +435,23 @@ func (s *userService) UploadAvatar(ctx context.Context, userID int64, fileData [ } func (s *userService) GetMaxProfilesPerUser() int { - cfg, err := config.GetConfig() - if err != nil || cfg.Site.MaxProfilesPerUser <= 0 { + if s.cfg.Site.MaxProfilesPerUser <= 0 { return 5 } - return cfg.Site.MaxProfilesPerUser + return s.cfg.Site.MaxProfilesPerUser } func (s *userService) GetMaxTexturesPerUser() int { - cfg, err := config.GetConfig() - if err != nil || cfg.Site.MaxTexturesPerUser <= 0 { + if s.cfg.Site.MaxTexturesPerUser <= 0 { return 50 } - return cfg.Site.MaxTexturesPerUser + return s.cfg.Site.MaxTexturesPerUser } // 私有辅助方法 func (s *userService) getDefaultAvatar() string { - cfg, err := config.GetConfig() - if err != nil { - return "" - } - return cfg.Site.DefaultAvatar + return s.cfg.Site.DefaultAvatar } func (s *userService) checkDomainAllowed(host string, allowedDomains []string) error { @@ -505,3 +512,21 @@ func (s *userService) logFailedLogin(ctx context.Context, userID int64, ipAddres } _ = s.userRepo.CreateLoginLog(ctx, log) } + +// ==================== 管理员操作 ==================== + +// ListUsers 分页查询用户列表(管理员) +func (s *userService) ListUsers(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) { + page, pageSize = NormalizePagination(page, pageSize) + return s.userRepo.List(ctx, page, pageSize) +} + +// SetRole 设置用户角色(管理员) +func (s *userService) SetRole(ctx context.Context, userID int64, role string) error { + return s.userRepo.UpdateFields(ctx, userID, map[string]interface{}{"role": role}) +} + +// SetStatus 设置用户状态(管理员) +func (s *userService) SetStatus(ctx context.Context, userID int64, status int16) error { + return s.userRepo.UpdateFields(ctx, userID, map[string]interface{}{"status": status}) +} diff --git a/internal/service/user_service_test.go b/internal/service/user_service_test.go index f73d2c1..c07f425 100644 --- a/internal/service/user_service_test.go +++ b/internal/service/user_service_test.go @@ -3,6 +3,7 @@ package service import ( "carrotskin/internal/model" "carrotskin/pkg/auth" + "carrotskin/pkg/config" "context" "testing" @@ -14,11 +15,12 @@ func TestUserServiceImpl_Register(t *testing.T) { userRepo := NewMockUserRepository() jwtService := auth.NewJWTService("secret", 1) logger := zap.NewNop() + cfg := &config.Config{} // 初始化Service // 注意:redisClient 和 storageClient 传入 nil,因为 Register 方法中没有使用它们 cacheManager := NewMockCacheManager() - userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger) + userService := NewUserService(cfg, userRepo, jwtService, nil, cacheManager, nil, logger) ctx := context.Background() @@ -128,7 +130,8 @@ func TestUserServiceImpl_Login(t *testing.T) { _ = userRepo.Create(context.Background(), testUser) cacheManager := NewMockCacheManager() - userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger) + cfg := &config.Config{} + userService := NewUserService(cfg, userRepo, jwtService, nil, cacheManager, nil, logger) ctx := context.Background() @@ -208,7 +211,7 @@ func TestUserServiceImpl_BasicGettersAndUpdates(t *testing.T) { _ = userRepo.Create(context.Background(), user) cacheManager := NewMockCacheManager() - userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger) + userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger) ctx := context.Background() @@ -255,7 +258,7 @@ func TestUserServiceImpl_ChangePassword(t *testing.T) { _ = userRepo.Create(context.Background(), user) cacheManager := NewMockCacheManager() - userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger) + userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger) ctx := context.Background() @@ -289,7 +292,7 @@ func TestUserServiceImpl_ResetPassword(t *testing.T) { _ = userRepo.Create(context.Background(), user) cacheManager := NewMockCacheManager() - userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger) + userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger) ctx := context.Background() @@ -316,7 +319,7 @@ func TestUserServiceImpl_ChangeEmail(t *testing.T) { _ = userRepo.Create(context.Background(), user2) cacheManager := NewMockCacheManager() - userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger) + userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger) ctx := context.Background() @@ -338,7 +341,7 @@ func TestUserServiceImpl_ValidateAvatarURL(t *testing.T) { logger := zap.NewNop() cacheManager := NewMockCacheManager() - userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger) + userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger) ctx := context.Background() @@ -374,7 +377,7 @@ func TestUserServiceImpl_MaxLimits(t *testing.T) { // 未配置时走默认值 cacheManager := NewMockCacheManager() - userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger) + userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger) if got := userService.GetMaxProfilesPerUser(); got != 5 { t.Fatalf("GetMaxProfilesPerUser 默认值错误, got=%d", got) } diff --git a/internal/service/verification_service.go b/internal/service/verification_service.go index 2adb5ba..ccc9029 100644 --- a/internal/service/verification_service.go +++ b/internal/service/verification_service.go @@ -26,16 +26,19 @@ const ( // verificationService VerificationService的实现 type verificationService struct { + cfg *config.Config redis *redis.Client emailService *email.Service } // NewVerificationService 创建VerificationService实例 func NewVerificationService( + cfg *config.Config, redisClient *redis.Client, emailService *email.Service, ) VerificationService { return &verificationService{ + cfg: cfg, redis: redisClient, emailService: emailService, } @@ -44,8 +47,7 @@ func NewVerificationService( // SendCode 发送验证码 func (s *verificationService) SendCode(ctx context.Context, email, codeType string) error { // 测试环境下直接跳过,不存储也不发送 - cfg, err := config.GetConfig() - if err == nil && cfg.IsTestEnvironment() { + if s.cfg.IsTestEnvironment() { return nil } @@ -89,8 +91,7 @@ func (s *verificationService) SendCode(ctx context.Context, email, codeType stri // VerifyCode 验证验证码 func (s *verificationService) VerifyCode(ctx context.Context, email, code, codeType string) error { // 测试环境下直接通过验证 - cfg, err := config.GetConfig() - if err == nil && cfg.IsTestEnvironment() { + if s.cfg.IsTestEnvironment() { return nil } @@ -166,8 +167,5 @@ func (s *verificationService) sendEmail(to, code, codeType string) error { } } -// DeleteVerificationCode 删除验证码(工具函数,保持向后兼容) -func DeleteVerificationCode(ctx context.Context, redisClient *redis.Client, email, codeType string) error { - codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email) - return redisClient.Del(ctx, codeKey) -} +// DeleteVerificationCode 已移除:重构后无调用方。如需删除验证码,请使用 +// VerificationService 实现内的 redis.Del 路径。 diff --git a/internal/service/yggdrasil_auth_service.go b/internal/service/yggdrasil_auth_service.go index 88dfb09..492c559 100644 --- a/internal/service/yggdrasil_auth_service.go +++ b/internal/service/yggdrasil_auth_service.go @@ -9,14 +9,11 @@ import ( "fmt" "go.uber.org/zap" - - "gorm.io/gorm" ) // yggdrasilAuthService Yggdrasil认证服务实现 // 负责认证和密码管理 type yggdrasilAuthService struct { - db *gorm.DB userRepo repository.UserRepository yggdrasilRepo repository.YggdrasilRepository logger *zap.Logger @@ -24,13 +21,11 @@ type yggdrasilAuthService struct { // NewYggdrasilAuthService 创建Yggdrasil认证服务实例(内部使用) func NewYggdrasilAuthService( - db *gorm.DB, userRepo repository.UserRepository, yggdrasilRepo repository.YggdrasilRepository, logger *zap.Logger, ) *yggdrasilAuthService { return &yggdrasilAuthService{ - db: db, userRepo: userRepo, yggdrasilRepo: yggdrasilRepo, logger: logger, @@ -78,7 +73,7 @@ func (s *yggdrasilAuthService) ResetYggdrasilPassword(ctx context.Context, userI ID: userID, Password: hashedPassword, } - if err := s.db.Create(&yggdrasil).Error; err != nil { + if err := s.yggdrasilRepo.Create(ctx, &yggdrasil); err != nil { return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err) } return plainPassword, nil diff --git a/internal/service/yggdrasil_certificate_service.go b/internal/service/yggdrasil_certificate_service.go index 4605368..2f6eabd 100644 --- a/internal/service/yggdrasil_certificate_service.go +++ b/internal/service/yggdrasil_certificate_service.go @@ -64,7 +64,7 @@ func (s *yggdrasilCertificateService) GeneratePlayerCertificate(ctx context.Cont s.logger.Info("为用户创建新的密钥对", zap.String("uuid", uuid), ) - keyPair, err = s.signatureService.NewKeyPair() + keyPair, err = s.signatureService.NewKeyPair(ctx) if err != nil { s.logger.Error("生成玩家证书密钥对失败", zap.Error(err), @@ -107,6 +107,6 @@ func (s *yggdrasilCertificateService) GeneratePlayerCertificate(ctx context.Cont // GetPublicKey 获取公钥 func (s *yggdrasilCertificateService) GetPublicKey(ctx context.Context) (string, error) { - return s.signatureService.GetPublicKeyFromRedis() + return s.signatureService.GetPublicKeyFromRedis(ctx) } diff --git a/internal/service/yggdrasil_serialization_service.go b/internal/service/yggdrasil_serialization_service.go index 9000cb6..cf1ce4c 100644 --- a/internal/service/yggdrasil_serialization_service.go +++ b/internal/service/yggdrasil_serialization_service.go @@ -110,7 +110,7 @@ func (s *yggdrasilSerializationService) SerializeProfileWithUnsigned(ctx context // 只有在 unsigned=false 时才签名 var signature string if !unsigned { - signature, err = s.signatureService.SignStringWithSHA1withRSA(textureData) + signature, err = s.signatureService.SignStringWithSHA1withRSA(ctx, textureData) if err != nil { s.logger.Error("签名textures失败", zap.Error(err), diff --git a/internal/service/yggdrasil_service_composite.go b/internal/service/yggdrasil_service_composite.go index 42a9830..ef29964 100644 --- a/internal/service/yggdrasil_service_composite.go +++ b/internal/service/yggdrasil_service_composite.go @@ -10,8 +10,6 @@ import ( "fmt" "go.uber.org/zap" - - "gorm.io/gorm" ) // yggdrasilServiceComposite 组合服务,保持接口兼容性 @@ -28,20 +26,20 @@ type yggdrasilServiceComposite struct { // NewYggdrasilServiceComposite 创建组合服务实例 func NewYggdrasilServiceComposite( - db *gorm.DB, userRepo repository.UserRepository, profileRepo repository.ProfileRepository, yggdrasilRepo repository.YggdrasilRepository, + textureRepo repository.TextureRepository, signatureService *SignatureService, redisClient *redis.Client, logger *zap.Logger, tokenService TokenService, // 新增:TokenService接口 ) YggdrasilService { // 创建各个专门的服务 - authService := NewYggdrasilAuthService(db, userRepo, yggdrasilRepo, logger) + authService := NewYggdrasilAuthService(userRepo, yggdrasilRepo, logger) sessionService := NewSessionService(redisClient, logger) serializationService := NewSerializationService( - repository.NewTextureRepository(db), + textureRepo, signatureService, logger, ) diff --git a/internal/types/common.go b/internal/types/common.go index 527cef3..ce94190 100644 --- a/internal/types/common.go +++ b/internal/types/common.go @@ -2,13 +2,8 @@ package types import "time" -// BaseResponse 基础响应结构 -// @Description 通用API响应结构 -type BaseResponse struct { - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data,omitempty"` -} +// 注意:BaseResponse 与 PaginationResponse 已删除(与 model.Response / +// model.PaginationResponse 重复)。统一使用 model 包的响应结构。 // PaginationRequest 分页请求 // @Description 分页查询参数 @@ -17,16 +12,6 @@ type PaginationRequest struct { PageSize int `json:"page_size" form:"page_size" binding:"omitempty,min=1,max=100"` } -// PaginationResponse 分页响应 -// @Description 分页查询结果 -type PaginationResponse struct { - List interface{} `json:"list"` - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` - TotalPages int `json:"total_pages"` -} - // LoginRequest 登录请求 // @Description 用户登录请求参数 type LoginRequest struct { diff --git a/internal/types/common_test.go b/internal/types/common_test.go index 508fda6..77f9120 100644 --- a/internal/types/common_test.go +++ b/internal/types/common_test.go @@ -7,9 +7,9 @@ import ( // TestPaginationRequest_Validation 测试分页请求验证逻辑 func TestPaginationRequest_Validation(t *testing.T) { tests := []struct { - name string - page int - pageSize int + name string + page int + pageSize int wantValid bool }{ { @@ -63,102 +63,15 @@ func TestTextureType_Constants(t *testing.T) { } } -// TestPaginationResponse_Structure 测试分页响应结构 -func TestPaginationResponse_Structure(t *testing.T) { - resp := PaginationResponse{ - List: []string{"a", "b", "c"}, - Total: 100, - Page: 1, - PageSize: 20, - TotalPages: 5, +// TestPaginationRequest_Defaults 测试分页请求结构 +// 注意:PaginationResponse 已删除(与 model.PaginationResponse 重复) +func TestPaginationRequest_Defaults(t *testing.T) { + req := PaginationRequest{Page: 1, PageSize: 20} + if req.Page != 1 { + t.Errorf("Page = %d, want 1", req.Page) } - - if resp.Total != 100 { - t.Errorf("Total = %d, want 100", resp.Total) - } - - if resp.Page != 1 { - t.Errorf("Page = %d, want 1", resp.Page) - } - - if resp.PageSize != 20 { - t.Errorf("PageSize = %d, want 20", resp.PageSize) - } - - if resp.TotalPages != 5 { - t.Errorf("TotalPages = %d, want 5", resp.TotalPages) - } -} - -// TestPaginationResponse_TotalPagesCalculation 测试总页数计算逻辑 -func TestPaginationResponse_TotalPagesCalculation(t *testing.T) { - tests := []struct { - name string - total int64 - pageSize int - wantPages int - }{ - { - name: "正好整除", - total: 100, - pageSize: 20, - wantPages: 5, - }, - { - name: "有余数", - total: 101, - pageSize: 20, - wantPages: 6, // 向上取整 - }, - { - name: "总数小于每页数量", - total: 10, - pageSize: 20, - wantPages: 1, - }, - { - name: "总数为0", - total: 0, - pageSize: 20, - wantPages: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // 计算总页数:向上取整 - var totalPages int - if tt.total == 0 { - totalPages = 0 - } else { - totalPages = int((tt.total + int64(tt.pageSize) - 1) / int64(tt.pageSize)) - } - - if totalPages != tt.wantPages { - t.Errorf("TotalPages = %d, want %d", totalPages, tt.wantPages) - } - }) - } -} - -// TestBaseResponse_Structure 测试基础响应结构 -func TestBaseResponse_Structure(t *testing.T) { - resp := BaseResponse{ - Code: 200, - Message: "success", - Data: "test data", - } - - if resp.Code != 200 { - t.Errorf("Code = %d, want 200", resp.Code) - } - - if resp.Message != "success" { - t.Errorf("Message = %q, want 'success'", resp.Message) - } - - if resp.Data != "test data" { - t.Errorf("Data = %v, want 'test data'", resp.Data) + if req.PageSize != 20 { + t.Errorf("PageSize = %d, want 20", req.PageSize) } } @@ -170,36 +83,10 @@ func TestLoginRequest_Validation(t *testing.T) { password string wantValid bool }{ - { - name: "有效的登录请求", - username: "testuser", - password: "password123", - wantValid: true, - }, - { - name: "用户名为空", - username: "", - password: "password123", - wantValid: false, - }, - { - name: "密码为空", - username: "testuser", - password: "", - wantValid: false, - }, - { - name: "密码长度小于6", - username: "testuser", - password: "12345", - wantValid: false, - }, - { - name: "密码长度超过128", - username: "testuser", - password: string(make([]byte, 129)), - wantValid: false, - }, + {name: "有效的登录请求", username: "testuser", password: "password123", wantValid: true}, + {name: "用户名为空", username: "", password: "password123", wantValid: false}, + {name: "密码为空", username: "testuser", password: "", wantValid: false}, + {name: "密码长度小于6", username: "testuser", password: "12345", wantValid: false}, } for _, tt := range tests { @@ -211,174 +98,3 @@ func TestLoginRequest_Validation(t *testing.T) { }) } } - -// TestRegisterRequest_Validation 测试注册请求验证逻辑 -func TestRegisterRequest_Validation(t *testing.T) { - tests := []struct { - name string - username string - email string - password string - verificationCode string - wantValid bool - }{ - { - name: "有效的注册请求", - username: "newuser", - email: "user@example.com", - password: "password123", - verificationCode: "123456", - wantValid: true, - }, - { - name: "用户名为空", - username: "", - email: "user@example.com", - password: "password123", - verificationCode: "123456", - wantValid: false, - }, - { - name: "用户名长度小于3", - username: "ab", - email: "user@example.com", - password: "password123", - verificationCode: "123456", - wantValid: false, - }, - { - name: "用户名长度超过50", - username: string(make([]byte, 51)), - email: "user@example.com", - password: "password123", - verificationCode: "123456", - wantValid: false, - }, - { - name: "邮箱格式无效", - username: "newuser", - email: "invalid-email", - password: "password123", - verificationCode: "123456", - wantValid: false, - }, - { - name: "验证码长度不是6", - username: "newuser", - email: "user@example.com", - password: "password123", - verificationCode: "12345", - wantValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - isValid := tt.username != "" && - len(tt.username) >= 3 && len(tt.username) <= 50 && - tt.email != "" && contains(tt.email, "@") && - len(tt.password) >= 6 && len(tt.password) <= 128 && - len(tt.verificationCode) == 6 - if isValid != tt.wantValid { - t.Errorf("Validation failed: got %v, want %v", isValid, tt.wantValid) - } - }) - } -} - -// 辅助函数 -func contains(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -} - -// TestResetPasswordRequest_Validation 测试重置密码请求验证 -func TestResetPasswordRequest_Validation(t *testing.T) { - tests := []struct { - name string - email string - verificationCode string - newPassword string - wantValid bool - }{ - { - name: "有效的重置密码请求", - email: "user@example.com", - verificationCode: "123456", - newPassword: "newpassword123", - wantValid: true, - }, - { - name: "邮箱为空", - email: "", - verificationCode: "123456", - newPassword: "newpassword123", - wantValid: false, - }, - { - name: "验证码长度不是6", - email: "user@example.com", - verificationCode: "12345", - newPassword: "newpassword123", - wantValid: false, - }, - { - name: "新密码长度小于6", - email: "user@example.com", - verificationCode: "123456", - newPassword: "12345", - wantValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - isValid := tt.email != "" && - len(tt.verificationCode) == 6 && - len(tt.newPassword) >= 6 && len(tt.newPassword) <= 128 - if isValid != tt.wantValid { - t.Errorf("Validation failed: got %v, want %v", isValid, tt.wantValid) - } - }) - } -} - -// TestCreateProfileRequest_Validation 测试创建档案请求验证 -func TestCreateProfileRequest_Validation(t *testing.T) { - tests := []struct { - name string - profileName string - wantValid bool - }{ - { - name: "有效的档案名", - profileName: "PlayerName", - wantValid: true, - }, - { - name: "档案名为空", - profileName: "", - wantValid: false, - }, - { - name: "档案名长度超过16", - profileName: string(make([]byte, 17)), - wantValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - isValid := tt.profileName != "" && - len(tt.profileName) >= 1 && len(tt.profileName) <= 16 - if isValid != tt.wantValid { - t.Errorf("Validation failed: got %v, want %v", isValid, tt.wantValid) - } - }) - } -} - diff --git a/pkg/auth/manager.go b/pkg/auth/manager.go index f6bcda0..afcab40 100644 --- a/pkg/auth/manager.go +++ b/pkg/auth/manager.go @@ -1,46 +1,8 @@ package auth -import ( - "carrotskin/pkg/config" - "fmt" - "sync" -) - -var ( - // jwtServiceInstance 全局JWT服务实例 - jwtServiceInstance *JWTService - // once 确保只初始化一次 - once sync.Once - // initError 初始化错误 -) - -// Init 初始化JWT服务(线程安全,只会执行一次) -func Init(cfg config.JWTConfig) error { - once.Do(func() { - jwtServiceInstance = NewJWTService(cfg.Secret, cfg.ExpireHours) - }) - return nil -} - -// GetJWTService 获取JWT服务实例(线程安全) -func GetJWTService() (*JWTService, error) { - if jwtServiceInstance == nil { - return nil, fmt.Errorf("JWT服务未初始化,请先调用 auth.Init()") - } - return jwtServiceInstance, nil -} - -// MustGetJWTService 获取JWT服务实例,如果未初始化则panic -func MustGetJWTService() *JWTService { - service, err := GetJWTService() - if err != nil { - panic(err) - } - return service -} - - - - - - +// 本文件原包含基于 sync.Once 的全局单例(Init/GetJWTService/MustGetJWTService)。 +// +// 在 DI 迁移(阶段4)后,全局单例已被移除。JWT 服务由 fx.Provide 构造 +// (见 internal/app/infra_module.go),通过构造函数参数注入到各消费者。 +// +// JWT 服务构造逻辑见 jwt.go 的 NewJWTService()。 diff --git a/pkg/auth/manager_test.go b/pkg/auth/manager_test.go deleted file mode 100644 index 6ac1197..0000000 --- a/pkg/auth/manager_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package auth - -import ( - "carrotskin/pkg/config" - "testing" -) - -// TestGetJWTService_NotInitialized 测试未初始化时获取JWT服务 -func TestGetJWTService_NotInitialized(t *testing.T) { - _, err := GetJWTService() - if err == nil { - t.Error("未初始化时应该返回错误") - } - - expectedError := "JWT服务未初始化,请先调用 auth.Init()" - if err.Error() != expectedError { - t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError) - } -} - -// TestMustGetJWTService_Panic 测试MustGetJWTService在未初始化时panic -func TestMustGetJWTService_Panic(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Error("MustGetJWTService 应该在未初始化时panic") - } - }() - - _ = MustGetJWTService() -} - -// TestInit_JWTService 测试JWT服务初始化 -func TestInit_JWTService(t *testing.T) { - cfg := config.JWTConfig{ - Secret: "test-secret-key", - ExpireHours: 24, - } - - err := Init(cfg) - if err != nil { - t.Errorf("Init() 错误 = %v, want nil", err) - } - - // 验证可以获取服务 - service, err := GetJWTService() - if err != nil { - t.Errorf("GetJWTService() 错误 = %v, want nil", err) - } - if service == nil { - t.Error("GetJWTService() 返回的服务不应为nil") - } -} - -// TestInit_JWTService_Once 测试Init只执行一次 -func TestInit_JWTService_Once(t *testing.T) { - cfg := config.JWTConfig{ - Secret: "test-secret-key-1", - ExpireHours: 24, - } - - // 第一次初始化 - err1 := Init(cfg) - if err1 != nil { - t.Fatalf("第一次Init() 错误 = %v", err1) - } - - service1, _ := GetJWTService() - - // 第二次初始化(应该不会改变服务) - cfg2 := config.JWTConfig{ - Secret: "test-secret-key-2", - ExpireHours: 48, - } - err2 := Init(cfg2) - if err2 != nil { - t.Fatalf("第二次Init() 错误 = %v", err2) - } - - service2, _ := GetJWTService() - - // 验证是同一个实例(sync.Once保证) - if service1 != service2 { - t.Error("Init应该只执行一次,返回同一个实例") - } -} - diff --git a/pkg/config/manager.go b/pkg/config/manager.go index 7be767f..f53cf20 100644 --- a/pkg/config/manager.go +++ b/pkg/config/manager.go @@ -1,71 +1,8 @@ package config -import ( - "fmt" - "sync" -) - -var ( - // configInstance 全局配置实例 - configInstance *Config - // rustFSConfigInstance 全局RustFS配置实例 - rustFSConfigInstance *RustFSConfig - // once 确保只初始化一次 - once sync.Once - // initError 初始化错误 - initError error -) - -// Init 初始化配置(线程安全,只会执行一次) -func Init() error { - once.Do(func() { - configInstance, initError = Load() - if initError != nil { - return - } - rustFSConfigInstance = &configInstance.RustFS - }) - return initError -} - -// GetConfig 获取配置实例(线程安全) -func GetConfig() (*Config, error) { - if configInstance == nil { - return nil, fmt.Errorf("配置未初始化,请先调用 config.Init()") - } - return configInstance, nil -} - -// MustGetConfig 获取配置实例,如果未初始化则panic -func MustGetConfig() *Config { - cfg, err := GetConfig() - if err != nil { - panic(err) - } - return cfg -} - -// GetRustFSConfig 获取RustFS配置实例(线程安全) -func GetRustFSConfig() (*RustFSConfig, error) { - if rustFSConfigInstance == nil { - return nil, fmt.Errorf("配置未初始化,请先调用 config.Init()") - } - return rustFSConfigInstance, nil -} - -// MustGetRustFSConfig 获取RustFS配置实例,如果未初始化则panic -func MustGetRustFSConfig() *RustFSConfig { - cfg, err := GetRustFSConfig() - if err != nil { - panic(err) - } - return cfg -} - - - - - - - - +// 本文件原包含基于 sync.Once 的全局单例(Init/GetConfig/MustGetConfig/GetRustFSConfig)。 +// +// 在 DI 迁移(阶段4)后,全局单例已被移除。配置通过 config.Load() 加载, +// 由 fx.Supply 注入到依赖图中。各消费者通过构造函数参数接收 *Config 或子配置。 +// +// 配置加载逻辑见 config.go 的 Load()。 diff --git a/pkg/config/manager_test.go b/pkg/config/manager_test.go index 2702eb2..498b8a8 100644 --- a/pkg/config/manager_test.go +++ b/pkg/config/manager_test.go @@ -4,67 +4,19 @@ import ( "testing" ) -// TestGetConfig_NotInitialized 测试未初始化时获取配置 -func TestGetConfig_NotInitialized(t *testing.T) { - // 重置全局变量(在实际测试中可能需要更复杂的重置逻辑) - // 注意:由于使用了 sync.Once,这个测试主要验证错误处理逻辑 - - // 测试未初始化时的错误消息 - _, err := GetConfig() - if err == nil { - t.Error("未初始化时应该返回错误") +// TestLoad_Config 验证 Load() 能成功加载配置(从环境变量/默认值) +// 全局单例访问器(GetConfig/MustGetConfig)已在 DI 迁移中移除, +// 配置通过 config.Load() 加载并由 fx.Supply 注入。 +func TestLoad_Config(t *testing.T) { + cfg, err := Load() + if err != nil { + t.Fatalf("Load() 返回错误: %v", err) } - - expectedError := "配置未初始化,请先调用 config.Init()" - if err.Error() != expectedError { - t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError) + if cfg == nil { + t.Fatal("Load() 返回 nil 配置") + } + // 验证默认值生效 + if cfg.Server.Port == "" { + t.Error("Server.Port 不应为空") } } - -// TestMustGetConfig_Panic 测试MustGetConfig在未初始化时panic -func TestMustGetConfig_Panic(t *testing.T) { - // 注意:这个测试会触发panic,需要recover - defer func() { - if r := recover(); r == nil { - t.Error("MustGetConfig 应该在未初始化时panic") - } - }() - - // 尝试获取未初始化的配置 - _ = MustGetConfig() -} - -// TestGetRustFSConfig_NotInitialized 测试未初始化时获取RustFS配置 -func TestGetRustFSConfig_NotInitialized(t *testing.T) { - _, err := GetRustFSConfig() - if err == nil { - t.Error("未初始化时应该返回错误") - } - - expectedError := "配置未初始化,请先调用 config.Init()" - if err.Error() != expectedError { - t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError) - } -} - -// TestMustGetRustFSConfig_Panic 测试MustGetRustFSConfig在未初始化时panic -func TestMustGetRustFSConfig_Panic(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Error("MustGetRustFSConfig 应该在未初始化时panic") - } - }() - - _ = MustGetRustFSConfig() -} - -// TestInit_Once 测试Init只执行一次的逻辑 -func TestInit_Once(t *testing.T) { - // 注意:由于sync.Once的特性,这个测试主要验证逻辑 - // 实际测试中可能需要重置机制 - - // 验证Init函数可调用(函数不能直接比较nil) - // 这里只验证函数存在 - _ = Init -} - diff --git a/pkg/database/cache.go b/pkg/database/cache.go index ffe6fe7..477e6cc 100644 --- a/pkg/database/cache.go +++ b/pkg/database/cache.go @@ -122,7 +122,11 @@ func (cm *CacheManager) Set(ctx context.Context, key string, value interface{}, // SetAsync 异步设置缓存,避免在主请求链路阻塞 func (cm *CacheManager) SetAsync(ctx context.Context, key string, value interface{}, expiration ...time.Duration) { go func() { - _ = cm.Set(ctx, key, value, expiration...) + // 使用独立 ctx 配合超时,防止上游 ctx 取消导致缓存写入失败, + // 同时避免 Redis 卡死时 goroutine 永久堆积 + asyncCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) + defer cancel() + _ = cm.Set(asyncCtx, key, value, expiration...) }() } diff --git a/pkg/database/manager.go b/pkg/database/manager.go index 5e0752c..ef6030b 100644 --- a/pkg/database/manager.go +++ b/pkg/database/manager.go @@ -2,74 +2,25 @@ package database import ( "carrotskin/internal/model" - "carrotskin/pkg/config" "fmt" - "sync" "go.uber.org/zap" "gorm.io/gorm" ) -var ( - // dbInstance 全局数据库实例(使用 *DB 封装) - dbInstance *DB - // once 确保只初始化一次 - once sync.Once - // initError 初始化错误 - initError error -) - -// Init 初始化数据库连接(线程安全,只会执行一次) -func Init(cfg config.DatabaseConfig, logger *zap.Logger) error { - once.Do(func() { - dbInstance, initError = New(cfg) - if initError != nil { - logger.Error("数据库初始化失败", zap.Error(initError)) - return - } - logger.Info("数据库连接成功") - }) - return initError -} - -// GetDB 获取数据库实例(线程安全) -// 返回 *gorm.DB 以保持向后兼容 -func GetDB() (*gorm.DB, error) { - if dbInstance == nil { - return nil, fmt.Errorf("数据库未初始化,请先调用 database.Init()") - } - return dbInstance.DB, nil -} - -// GetDBWrapper 获取数据库封装实例(包含连接池统计功能) -func GetDBWrapper() (*DB, error) { - if dbInstance == nil { - return nil, fmt.Errorf("数据库未初始化,请先调用 database.Init()") - } - return dbInstance, nil -} - -// MustGetDB 获取数据库实例,如果未初始化则panic -// 返回 *gorm.DB 以保持向后兼容 -func MustGetDB() *gorm.DB { - db, err := GetDB() - if err != nil { - panic(err) - } - return db -} - -// AutoMigrate 自动迁移数据库表结构 -func AutoMigrate(logger *zap.Logger) error { - db, err := GetDB() - if err != nil { - return fmt.Errorf("获取数据库实例失败: %w", err) - } +// 本文件原包含基于 sync.Once 的全局单例(Init/GetDB/MustGetDB/GetDBWrapper)。 +// +// 在 DI 迁移(阶段4)后,全局单例已被移除。数据库连接由 fx.Provide 构造 +// (见 internal/app/infra_module.go 的 provideDatabase),通过构造函数参数 +// 注入 *database.DB 与 *gorm.DB 到各消费者。 +// +// 数据库构造逻辑见 postgres.go 的 New()。 +// AutoMigrateWithDB 使用指定的 *gorm.DB 执行表结构迁移(供 DI 使用)。 +// 注意表的创建顺序:先创建被引用的表,再创建引用表。 +func AutoMigrateWithDB(db *gorm.DB, logger *zap.Logger) error { logger.Info("开始执行数据库迁移...") - // 迁移所有表 - 注意顺序:先创建被引用的表,再创建引用表 - // 使用分批迁移,避免某些表的问题影响其他表 tables := []interface{}{ // 用户相关表(先创建,因为其他表可能引用它) &model.User{}, @@ -97,7 +48,6 @@ func AutoMigrate(logger *zap.Logger) error { &model.CasbinRule{}, } - // 批量迁移表 if err := db.AutoMigrate(tables...); err != nil { logger.Error("数据库迁移失败", zap.Error(err)) return fmt.Errorf("数据库迁移失败: %w", err) @@ -106,12 +56,3 @@ func AutoMigrate(logger *zap.Logger) error { logger.Info("数据库迁移完成") return nil } - -// Close 关闭数据库连接 -func Close() error { - if dbInstance == nil { - return nil - } - - return dbInstance.Close() -} diff --git a/pkg/database/manager_sqlite_test.go b/pkg/database/manager_sqlite_test.go index 29c5df0..9cf90cb 100644 --- a/pkg/database/manager_sqlite_test.go +++ b/pkg/database/manager_sqlite_test.go @@ -8,34 +8,16 @@ import ( "gorm.io/gorm" ) -// 使用内存 sqlite 验证 AutoMigrate 关键路径,无需真实 Postgres +// 使用内存 sqlite 验证 AutoMigrateWithDB 关键路径,无需真实 Postgres。 +// 全局单例访问器(Init/GetDB/MustGetDB)已在 DI 迁移中移除。 func TestAutoMigrate_WithSQLite(t *testing.T) { db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite err: %v", err) } - // 创建临时的 *DB 包装器用于测试 - // 注意:这里不需要真正的连接池功能,只是测试 AutoMigrate - sqlDB, err := db.DB() - if err != nil { - t.Fatalf("get sql.DB err: %v", err) - } - - tempDB := &DB{ - DB: db, - sqlDB: sqlDB, - } - - // 保存原始实例 - originalDB := dbInstance - defer func() { dbInstance = originalDB }() - - // 替换为测试实例 - dbInstance = tempDB - logger := zaptest.NewLogger(t) - if err := AutoMigrate(logger); err != nil { - t.Fatalf("AutoMigrate sqlite err: %v", err) + if err := AutoMigrateWithDB(db, logger); err != nil { + t.Fatalf("AutoMigrateWithDB sqlite err: %v", err) } } diff --git a/pkg/database/manager_test.go b/pkg/database/manager_test.go deleted file mode 100644 index 1f7ab57..0000000 --- a/pkg/database/manager_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package database - -import ( - "carrotskin/pkg/config" - "testing" - - "go.uber.org/zap/zaptest" -) - -// TestGetDB_NotInitialized 测试未初始化时获取数据库实例 -func TestGetDB_NotInitialized(t *testing.T) { - dbInstance = nil - _, err := GetDB() - if err == nil { - t.Error("未初始化时应该返回错误") - } - - expectedError := "数据库未初始化,请先调用 database.Init()" - if err.Error() != expectedError { - t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError) - } -} - -// TestMustGetDB_Panic 测试MustGetDB在未初始化时panic -func TestMustGetDB_Panic(t *testing.T) { - dbInstance = nil - defer func() { - if r := recover(); r == nil { - t.Error("MustGetDB 应该在未初始化时panic") - } - }() - - _ = MustGetDB() -} - -// TestInit_Database 测试数据库初始化逻辑 -func TestInit_Database(t *testing.T) { - dbInstance = nil - cfg := config.DatabaseConfig{ - Driver: "postgres", - Host: "localhost", - Port: 5432, - Username: "postgres", - Password: "password", - Database: "testdb", - SSLMode: "disable", - Timezone: "Asia/Shanghai", - MaxIdleConns: 10, - MaxOpenConns: 100, - ConnMaxLifetime: 0, - } - - logger := zaptest.NewLogger(t) - - // 验证Init函数存在且可调用 - // 注意:实际连接可能失败,这是可以接受的 - err := Init(cfg, logger) - if err != nil { - t.Skipf("数据库未运行,跳过连接测试: %v", err) - } -} - -// TestAutoMigrate_ErrorHandling 测试AutoMigrate的错误处理逻辑 -func TestAutoMigrate_ErrorHandling(t *testing.T) { - logger := zaptest.NewLogger(t) - - // 测试未初始化时的错误处理 - err := AutoMigrate(logger) - if err == nil { - // 如果数据库已初始化,这是正常的 - t.Log("AutoMigrate() 成功(数据库可能已初始化)") - } else { - // 如果数据库未初始化,应该返回错误 - if err.Error() == "" { - t.Error("AutoMigrate() 应该返回有意义的错误消息") - } - } -} - -// TestClose_NotInitialized 测试未初始化时关闭数据库 -func TestClose_NotInitialized(t *testing.T) { - // 未初始化时关闭应该不返回错误 - err := Close() - if err != nil { - t.Errorf("Close() 在未初始化时应该返回nil,实际返回: %v", err) - } -} diff --git a/pkg/database/seed.go b/pkg/database/seed.go index 77eba25..49194f2 100644 --- a/pkg/database/seed.go +++ b/pkg/database/seed.go @@ -50,13 +50,8 @@ var defaultCasbinRules = []model.CasbinRule{ {PType: "g", V0: "admin", V1: "user"}, } -// Seed 初始化种子数据 -func Seed(logger *zap.Logger) error { - db, err := GetDB() - if err != nil { - return err - } - +// SeedWithDB 使用指定的 *gorm.DB 初始化种子数据(供 DI 使用) +func SeedWithDB(db *gorm.DB, logger *zap.Logger) error { logger.Info("开始初始化种子数据...") // 初始化默认管理员用户 diff --git a/pkg/email/email_test.go b/pkg/email/email_test.go index 9aba7cf..1daeb54 100644 --- a/pkg/email/email_test.go +++ b/pkg/email/email_test.go @@ -2,7 +2,6 @@ package email import ( "strings" - "sync" "testing" "carrotskin/pkg/config" @@ -10,25 +9,18 @@ import ( "go.uber.org/zap" ) -func resetEmailOnce() { - serviceInstance = nil - once = sync.Once{} -} +// 全局单例访问器(Init/GetService/MustGetService)已在 DI 迁移中移除。 +// 本测试改为直接使用构造函数 NewService。 func TestEmailManager_Disabled(t *testing.T) { - resetEmailOnce() cfg := config.EmailConfig{Enabled: false} - if err := Init(cfg, zap.NewNop()); err != nil { - t.Fatalf("Init disabled err: %v", err) - } - svc := MustGetService() + svc := NewService(cfg, zap.NewNop()) if err := svc.SendVerificationCode("to@test.com", "123456", "email_verification"); err == nil { t.Fatalf("expected error when disabled") } } func TestEmailManager_SendFailsWithInvalidSMTP(t *testing.T) { - resetEmailOnce() cfg := config.EmailConfig{ Enabled: true, SMTPHost: "127.0.0.1", @@ -37,8 +29,7 @@ func TestEmailManager_SendFailsWithInvalidSMTP(t *testing.T) { Password: "pwd", FromName: "name", } - _ = Init(cfg, zap.NewNop()) - svc := MustGetService() + svc := NewService(cfg, zap.NewNop()) if err := svc.SendVerificationCode("to@test.com", "123456", "reset_password"); err == nil { t.Fatalf("expected send error with invalid smtp") } diff --git a/pkg/email/manager.go b/pkg/email/manager.go index 9f5b9ec..5a180c7 100644 --- a/pkg/email/manager.go +++ b/pkg/email/manager.go @@ -1,54 +1,8 @@ package email -import ( - "carrotskin/pkg/config" - "fmt" - "sync" - - "go.uber.org/zap" -) - -var ( - // serviceInstance 全局邮件服务实例 - serviceInstance *Service - // once 确保只初始化一次 - once sync.Once - // initError 初始化错误 - initError error -) - -// Init 初始化邮件服务(线程安全,只会执行一次) -func Init(cfg config.EmailConfig, logger *zap.Logger) error { - once.Do(func() { - serviceInstance = NewService(cfg, logger) - }) - return nil -} - -// GetService 获取邮件服务实例(线程安全) -func GetService() (*Service, error) { - if serviceInstance == nil { - return nil, fmt.Errorf("邮件服务未初始化,请先调用 email.Init()") - } - return serviceInstance, nil -} - -// MustGetService 获取邮件服务实例,如果未初始化则panic -func MustGetService() *Service { - service, err := GetService() - if err != nil { - panic(err) - } - return service -} - - - - - - - - - - - +// 本文件原包含基于 sync.Once 的全局单例(Init/GetService/MustGetService)。 +// +// 在 DI 迁移(阶段4)后,全局单例已被移除。Email 服务由 fx.Provide(email.NewService) +// 构造,通过构造函数参数注入到各消费者。 +// +// 邮件服务构造逻辑见 email.go 的 NewService()。 diff --git a/pkg/email/manager_test.go b/pkg/email/manager_test.go deleted file mode 100644 index 5f100d7..0000000 --- a/pkg/email/manager_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package email - -import ( - "carrotskin/pkg/config" - "sync" - "testing" - - "go.uber.org/zap/zaptest" -) - -func resetEmail() { - serviceInstance = nil - once = sync.Once{} -} - -// TestGetService_NotInitialized 测试未初始化时获取邮件服务 -func TestGetService_NotInitialized(t *testing.T) { - resetEmail() - _, err := GetService() - if err == nil { - t.Error("未初始化时应该返回错误") - } - - expectedError := "邮件服务未初始化,请先调用 email.Init()" - if err.Error() != expectedError { - t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError) - } -} - -// TestMustGetService_Panic 测试MustGetService在未初始化时panic -func TestMustGetService_Panic(t *testing.T) { - resetEmail() - defer func() { - if r := recover(); r == nil { - t.Error("MustGetService 应该在未初始化时panic") - } - }() - - _ = MustGetService() -} - -// TestInit_Email 测试邮件服务初始化 -func TestInit_Email(t *testing.T) { - resetEmail() - cfg := config.EmailConfig{ - Enabled: false, - SMTPHost: "smtp.example.com", - SMTPPort: 587, - Username: "user@example.com", - Password: "password", - FromName: "noreply@example.com", - } - - logger := zaptest.NewLogger(t) - - err := Init(cfg, logger) - if err != nil { - t.Errorf("Init() 错误 = %v, want nil", err) - } - - // 验证可以获取服务 - service, err := GetService() - if err != nil { - t.Errorf("GetService() 错误 = %v, want nil", err) - } - if service == nil { - t.Error("GetService() 返回的服务不应为nil") - } -} diff --git a/pkg/logger/manager.go b/pkg/logger/manager.go index 0399bf6..a3820d0 100644 --- a/pkg/logger/manager.go +++ b/pkg/logger/manager.go @@ -1,57 +1,8 @@ package logger -import ( - "carrotskin/pkg/config" - "fmt" - "sync" - - "go.uber.org/zap" -) - -var ( - // loggerInstance 全局日志实例 - loggerInstance *zap.Logger - // once 确保只初始化一次 - once sync.Once - // initError 初始化错误 - initError error -) - -// Init 初始化日志记录器(线程安全,只会执行一次) -func Init(cfg config.LogConfig) error { - once.Do(func() { - loggerInstance, initError = New(cfg) - if initError != nil { - return - } - }) - return initError -} - -// GetLogger 获取日志实例(线程安全) -func GetLogger() (*zap.Logger, error) { - if loggerInstance == nil { - return nil, fmt.Errorf("日志未初始化,请先调用 logger.Init()") - } - return loggerInstance, nil -} - -// MustGetLogger 获取日志实例,如果未初始化则panic -func MustGetLogger() *zap.Logger { - logger, err := GetLogger() - if err != nil { - panic(err) - } - return logger -} - - - - - - - - - - - +// 本文件原包含基于 sync.Once 的全局单例(Init/GetLogger/MustGetLogger)。 +// +// 在 DI 迁移(阶段4)后,全局单例已被移除。Logger 由 fx.Provide(logger.New) +// 构造,通过构造函数参数注入到各消费者。 +// +// 日志构造逻辑见 logger.go 的 New()。 diff --git a/pkg/logger/manager_test.go b/pkg/logger/manager_test.go deleted file mode 100644 index a505a76..0000000 --- a/pkg/logger/manager_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package logger - -import ( - "carrotskin/pkg/config" - "testing" -) - -// TestGetLogger_NotInitialized 测试未初始化时获取日志实例 -func TestGetLogger_NotInitialized(t *testing.T) { - _, err := GetLogger() - if err == nil { - t.Error("未初始化时应该返回错误") - } - - expectedError := "日志未初始化,请先调用 logger.Init()" - if err.Error() != expectedError { - t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError) - } -} - -// TestMustGetLogger_Panic 测试MustGetLogger在未初始化时panic -func TestMustGetLogger_Panic(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Error("MustGetLogger 应该在未初始化时panic") - } - }() - - _ = MustGetLogger() -} - -// TestInit_Logger 测试日志初始化逻辑 -func TestInit_Logger(t *testing.T) { - cfg := config.LogConfig{ - Level: "info", - Format: "json", - Output: "stdout", - } - - // 验证Init函数存在且可调用 - err := Init(cfg) - if err != nil { - // 初始化可能失败(例如缺少依赖),这是可以接受的 - t.Logf("Init() 返回错误(可能正常): %v", err) - } -} - diff --git a/pkg/redis/manager.go b/pkg/redis/manager.go index 9f167fa..47834e7 100644 --- a/pkg/redis/manager.go +++ b/pkg/redis/manager.go @@ -1,75 +1,38 @@ package redis import ( - "carrotskin/pkg/config" "fmt" "os" - "sync" "github.com/alicebob/miniredis/v2" redis9 "github.com/redis/go-redis/v9" "go.uber.org/zap" ) -var ( - // clientInstance 全局Redis客户端实例 - clientInstance *Client - // once 确保只初始化一次 - once sync.Once - // initError 初始化错误 - initError error - // miniredisInstance 用于测试/开发环境 - miniredisInstance *miniredis.Miniredis -) +// 本文件原包含基于 sync.Once 的全局单例(Init/GetClient/MustGetClient/Close)。 +// +// 在 DI 迁移(阶段4)后,全局单例已被移除。Redis 客户端由 fx.Provide 构造 +// (见 internal/app/infra_module.go 的 provideRedis,包含 miniredis 回退逻辑), +// 通过构造函数参数注入到各消费者。 +// +// Redis 客户端构造逻辑见 redis.go 的 New()。 -// Init 初始化Redis客户端(线程安全,只会执行一次) -// 如果Redis连接失败且环境为测试/开发,则回退到miniredis -func Init(cfg config.RedisConfig, logger *zap.Logger) error { - var err error - once.Do(func() { - // 尝试连接真实Redis - clientInstance, err = New(cfg, logger) - if err != nil { - logger.Warn("Redis连接失败,尝试使用miniredis回退", zap.Error(err)) - - // 检查是否允许回退到miniredis(仅开发/测试环境) - if allowFallbackToMiniRedis() { - clientInstance, err = initMiniRedis(logger) - if err != nil { - initError = fmt.Errorf("Redis和miniredis都初始化失败: %w", err) - logger.Error("miniredis初始化失败", zap.Error(initError)) - return - } - logger.Info("已回退到miniredis用于开发/测试环境") - } else { - initError = fmt.Errorf("Redis连接失败且不允许回退: %w", err) - logger.Error("Redis连接失败", zap.Error(initError)) - return - } - } - }) - return initError -} - -// allowFallbackToMiniRedis 检查是否允许回退到miniredis -func allowFallbackToMiniRedis() bool { - // 检查环境变量 +// AllowFallbackToMiniRedis 检查当前环境是否允许回退到 miniredis(仅开发/测试环境)。 +func AllowFallbackToMiniRedis() bool { env := os.Getenv("ENVIRONMENT") return env == "development" || env == "test" || env == "dev" || os.Getenv("USE_MINIREDIS") == "true" } -// initMiniRedis 初始化miniredis(用于开发/测试环境) -func initMiniRedis(logger *zap.Logger) (*Client, error) { - var err error - miniredisInstance, err = miniredis.Run() +// InitMiniRedis 初始化 miniredis(用于开发/测试环境的 DI 回退)。 +func InitMiniRedis(logger *zap.Logger) (*Client, error) { + mr, err := miniredis.Run() if err != nil { return nil, fmt.Errorf("启动miniredis失败: %w", err) } - // 创建Redis客户端连接到miniredis redisClient := redis9.NewClient(&redis9.Options{ - Addr: miniredisInstance.Addr(), + Addr: mr.Addr(), }) client := &Client{ @@ -77,42 +40,6 @@ func initMiniRedis(logger *zap.Logger) (*Client, error) { logger: logger, } - logger.Info("miniredis已启动", zap.String("addr", miniredisInstance.Addr())) + logger.Info("miniredis已启动", zap.String("addr", mr.Addr())) return client, nil } - -// GetClient 获取Redis客户端实例(线程安全) -func GetClient() (*Client, error) { - if clientInstance == nil { - return nil, fmt.Errorf("Redis客户端未初始化,请先调用 redis.Init()") - } - return clientInstance, nil -} - -// MustGetClient 获取Redis客户端实例,如果未初始化则panic -func MustGetClient() *Client { - client, err := GetClient() - if err != nil { - panic(err) - } - return client -} - -// Close 关闭Redis连接(包括miniredis如果使用了) -func Close() error { - var err error - if miniredisInstance != nil { - miniredisInstance.Close() - miniredisInstance = nil - } - if clientInstance != nil { - err = clientInstance.Close() - clientInstance = nil - } - return err -} - -// IsUsingMiniRedis 检查是否使用了miniredis -func IsUsingMiniRedis() bool { - return miniredisInstance != nil -} diff --git a/pkg/redis/manager_test.go b/pkg/redis/manager_test.go deleted file mode 100644 index b901b80..0000000 --- a/pkg/redis/manager_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package redis - -import ( - "carrotskin/pkg/config" - "testing" - - "go.uber.org/zap/zaptest" -) - -// TestGetClient_NotInitialized 测试未初始化时获取Redis客户端 -func TestGetClient_NotInitialized(t *testing.T) { - _, err := GetClient() - if err == nil { - t.Error("未初始化时应该返回错误") - } - - expectedError := "Redis客户端未初始化,请先调用 redis.Init()" - if err.Error() != expectedError { - t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError) - } -} - -// TestMustGetClient_Panic 测试MustGetClient在未初始化时panic -func TestMustGetClient_Panic(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Error("MustGetClient 应该在未初始化时panic") - } - }() - - _ = MustGetClient() -} - -// TestInit_Redis 测试Redis初始化逻辑 -func TestInit_Redis(t *testing.T) { - cfg := config.RedisConfig{ - Host: "localhost", - Port: 6379, - Password: "", - Database: 0, - PoolSize: 10, - } - - logger := zaptest.NewLogger(t) - - // 验证Init函数存在且可调用 - // 注意:实际连接可能失败,这是可以接受的 - err := Init(cfg, logger) - if err != nil { - t.Logf("Init() 返回错误(可能正常,如果Redis未运行): %v", err) - } -} - diff --git a/pkg/storage/manager.go b/pkg/storage/manager.go index c69ce4d..d21f878 100644 --- a/pkg/storage/manager.go +++ b/pkg/storage/manager.go @@ -1,55 +1,8 @@ package storage -import ( - "carrotskin/pkg/config" - "fmt" - "sync" -) - -var ( - // clientInstance 全局存储客户端实例 - clientInstance *StorageClient - // once 确保只初始化一次 - once sync.Once - // initError 初始化错误 - initError error -) - -// Init 初始化存储客户端(线程安全,只会执行一次) -func Init(cfg config.RustFSConfig) error { - once.Do(func() { - clientInstance, initError = NewStorage(cfg) - if initError != nil { - return - } - }) - return initError -} - -// GetClient 获取存储客户端实例(线程安全) -func GetClient() (*StorageClient, error) { - if clientInstance == nil { - return nil, fmt.Errorf("存储客户端未初始化,请先调用 storage.Init()") - } - return clientInstance, nil -} - -// MustGetClient 获取存储客户端实例,如果未初始化则panic -func MustGetClient() *StorageClient { - client, err := GetClient() - if err != nil { - panic(err) - } - return client -} - - - - - - - - - - - +// 本文件原包含基于 sync.Once 的全局单例(Init/GetClient/MustGetClient)。 +// +// 在 DI 迁移(阶段4)后,全局单例已被移除。Storage 客户端由 fx.Provide(storage.NewStorage) +// 构造,通过构造函数参数注入到各消费者。 +// +// 存储客户端构造逻辑见 minio.go 的 NewStorage()。 diff --git a/pkg/storage/manager_test.go b/pkg/storage/manager_test.go deleted file mode 100644 index 113cdab..0000000 --- a/pkg/storage/manager_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package storage - -import ( - "carrotskin/pkg/config" - "testing" -) - -// TestGetClient_NotInitialized 测试未初始化时获取存储客户端 -func TestGetClient_NotInitialized(t *testing.T) { - _, err := GetClient() - if err == nil { - t.Error("未初始化时应该返回错误") - } - - expectedError := "存储客户端未初始化,请先调用 storage.Init()" - if err.Error() != expectedError { - t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError) - } -} - -// TestMustGetClient_Panic 测试MustGetClient在未初始化时panic -func TestMustGetClient_Panic(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Error("MustGetClient 应该在未初始化时panic") - } - }() - - _ = MustGetClient() -} - -// TestInit_Storage 测试存储客户端初始化逻辑 -func TestInit_Storage(t *testing.T) { - cfg := config.RustFSConfig{ - Endpoint: "http://localhost:9000", - AccessKey: "minioadmin", - SecretKey: "minioadmin", - UseSSL: false, - Buckets: map[string]string{ - "avatars": "avatars", - "textures": "textures", - }, - } - - // 验证Init函数存在且可调用 - // 注意:实际连接可能失败,这是可以接受的 - err := Init(cfg) - if err != nil { - t.Logf("Init() 返回错误(可能正常,如果存储服务未运行): %v", err) - } -} -