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