refactor: 移除全局单例、修复分层违规、统一错误与响应
This commit is contained in:
@@ -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()。
|
||||
|
||||
@@ -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应该只执行一次,返回同一个实例")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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...)
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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("开始初始化种子数据...")
|
||||
|
||||
// 初始化默认管理员用户
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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()。
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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()。
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()。
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user