refactor: 移除全局单例、修复分层违规、统一错误与响应
Some checks failed
Build / build (push) Successful in 7m52s
Build / build-docker (push) Has been cancelled

This commit is contained in:
lafay
2026-06-15 16:40:36 +08:00
parent d9de39a0a3
commit 7d1c78f965
55 changed files with 593 additions and 1744 deletions

View File

@@ -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()。

View File

@@ -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应该只执行一次返回同一个实例")
}
}

View File

@@ -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()。

View File

@@ -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
}

View File

@@ -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...)
}()
}

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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("开始初始化种子数据...")
// 初始化默认管理员用户

View File

@@ -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")
}

View File

@@ -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()。

View File

@@ -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")
}
}

View File

@@ -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()。

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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()。

View File

@@ -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)
}
}