feat(app): 引入 uber-go/fx 重构 DI 装配层

This commit is contained in:
lafay
2026-06-15 16:39:45 +08:00
parent a111872b32
commit d9de39a0a3
11 changed files with 348 additions and 158 deletions

View File

@@ -0,0 +1,138 @@
package app
import (
"context"
"net/http"
"carrotskin/internal/container"
"carrotskin/internal/handler"
"carrotskin/internal/middleware"
"carrotskin/internal/task"
"carrotskin/pkg/auth"
"carrotskin/pkg/config"
"carrotskin/pkg/database"
"carrotskin/pkg/email"
"carrotskin/pkg/redis"
"carrotskin/pkg/storage"
"github.com/gin-gonic/gin"
"go.uber.org/fx"
"go.uber.org/zap"
"gorm.io/gorm"
)
// BootstrapModule 注册 Container、路由注册、HTTP 服务器与后台任务的生命周期。
//
// 设计说明:当前所有 Handler 仍依赖 *container.Container阶段6将逐步解耦为
// 直接依赖各 Service 接口)。本 Module 由 fx 注入基础设施,聚合为 Container
// 再用它注册路由、启动服务器与后台任务。当 fx.Run() 接收到 SIGINT/SIGTERM 时,
// 会按依赖逆序执行所有 OnStop 钩子,实现优雅关闭。
var BootstrapModule = fx.Options(
// Gin 路由引擎
fx.Provide(provideRouter),
// 由 fx 注入的基础设施聚合为 ContainerContainer 内部会构造 Repository/Service
fx.Provide(provideContainer),
// 数据库迁移与种子数据infra 就绪后执行)
fx.Invoke(runMigrate),
// 注册所有 HTTP 路由
fx.Invoke(registerRoutes),
// HTTP 服务器生命周期(启动/优雅关闭)
fx.Invoke(registerServerLifecycle),
// 后台任务调度器生命周期
fx.Invoke(registerTaskLifecycle),
)
// provideRouter 创建 Gin 引擎并挂载全局中间件。
func provideRouter(cfg *config.Config, logger *zap.Logger) *gin.Engine {
if cfg.Server.Mode == "production" {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
// 禁用自动重定向允许API路径带或不带/结尾都能正常访问
router.RedirectTrailingSlash = false
router.RedirectFixedPath = false
router.Use(middleware.Logger(logger))
router.Use(middleware.Recovery(logger))
router.Use(middleware.CORS(cfg))
return router
}
// provideContainer 将所有 fx 管理的基础设施聚合为 Container。
// Container.NewContainer 内部会基于这些 infra 构造 Repository 与 Service。
// 注意email.Service 可能为 nil当 EMAIL_ENABLED=false 时Container 会降级处理。
func provideContainer(
cfg *config.Config,
db *gorm.DB,
redisClient *redis.Client,
logger *zap.Logger,
jwt *auth.JWTService,
casbin *auth.CasbinService,
storageClient *storage.StorageClient,
emailService *email.Service,
) *container.Container {
return container.NewContainer(
cfg, db, redisClient, logger, jwt, casbin, storageClient, emailService,
)
}
// runMigrate 执行数据库迁移与种子数据初始化。
func runMigrate(db *gorm.DB, logger *zap.Logger) error {
if err := database.AutoMigrateWithDB(db, logger); err != nil {
return err
}
return database.SeedWithDB(db, logger)
}
// registerRoutes 注册所有 HTTP 路由。
func registerRoutes(router *gin.Engine, c *container.Container) {
handler.RegisterRoutesWithDI(router, c)
}
// registerServerLifecycle 注册 HTTP 服务器的启动与优雅关闭。
func registerServerLifecycle(lc fx.Lifecycle, router *gin.Engine, cfg *config.Config, logger *zap.Logger) {
srv := &http.Server{
Addr: cfg.Server.Port,
Handler: router,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
logger.Info("服务器启动", zap.String("port", cfg.Server.Port))
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal("服务器启动失败", zap.Error(err))
}
}()
return nil
},
OnStop: func(ctx context.Context) error {
logger.Info("正在关闭服务器...")
return srv.Shutdown(ctx)
},
})
}
// registerTaskLifecycle 注册后台任务调度器的启动与停止。
func registerTaskLifecycle(lc fx.Lifecycle, logger *zap.Logger) {
runner := task.NewRunner(logger)
ctx, cancel := context.WithCancel(context.Background())
lc.Append(fx.Hook{
OnStart: func(_ context.Context) error {
runner.Start(ctx)
return nil
},
OnStop: func(_ context.Context) error {
cancel()
runner.Wait()
return nil
},
})
}

View File

@@ -0,0 +1,8 @@
package app
// HandlerModule 已在阶段4移除。
//
// 当前阶段Handler 仍由 internal/container.Container + handler.RegisterRoutesWithDI
// 构造与注册(见 container_module.go 的 registerRoutes
//
// TODO(阶段6): Handler 将改为直接依赖各 Service 接口,由 fx 直接 Provide 与注册。

View File

@@ -0,0 +1,108 @@
package app
import (
"context"
"carrotskin/pkg/auth"
"carrotskin/pkg/config"
"carrotskin/pkg/database"
"carrotskin/pkg/email"
"carrotskin/pkg/logger"
"carrotskin/pkg/redis"
"carrotskin/pkg/storage"
"go.uber.org/fx"
"go.uber.org/zap"
"gorm.io/gorm"
)
// InfraModule 注册所有基础设施依赖Logger/DB/Redis/Storage/Email/JWT/Casbin。
// 每个需要资源释放的组件都通过 fx.Lifecycle 注册 OnStop 钩子,
// 彻底替代 main.go 里分散的 defer xxx.Close()。
var InfraModule = fx.Options(
// Logger
fx.Provide(logger.New),
// Database: 同时暴露 *database.DB封装带连接池统计和 *gorm.DB裸 GORM
fx.Provide(provideDatabase),
// 暴露 *gorm.DB 供 Repository/Casbin 等消费
fx.Provide(func(d *database.DB) *gorm.DB { return d.DB }),
// Redis含 miniredis 测试回退)
fx.Provide(provideRedis),
// 对象存储S3 兼容)
fx.Provide(provideStorage),
// 邮件服务
fx.Provide(email.NewService),
// JWT 服务(应用层认证)
fx.Provide(func(cfg *config.Config) *auth.JWTService {
return auth.NewJWTService(cfg.JWT.Secret, cfg.JWT.ExpireHours)
}),
// Casbin 权限服务
fx.Provide(func(db *gorm.DB, cfg *config.Config, log *zap.Logger) (*auth.CasbinService, error) {
return auth.NewCasbinService(db, cfg.Casbin.ModelPath, log)
}),
)
// provideDatabase 创建数据库连接并注册关闭钩子。
func provideDatabase(cfg *config.Config, lc fx.Lifecycle, log *zap.Logger) (*database.DB, error) {
db, err := database.New(cfg.Database)
if err != nil {
return nil, err
}
lc.Append(fx.Hook{
OnStop: func(ctx context.Context) error {
log.Info("正在关闭数据库连接")
return db.Close()
},
})
return db, nil
}
// provideRedis 创建 Redis 客户端并注册关闭钩子。
// 开发/测试环境下若连接失败会自动回退到 miniredis。
func provideRedis(cfg *config.Config, lc fx.Lifecycle, log *zap.Logger) (*redis.Client, error) {
client, err := redis.New(cfg.Redis, log)
if err != nil {
// 检查是否允许回退到 miniredis仅开发/测试环境)
if redis.AllowFallbackToMiniRedis() {
log.Warn("Redis连接失败尝试使用miniredis回退", zap.Error(err))
client, err = redis.InitMiniRedis(log)
if err != nil {
return nil, err
}
log.Info("已回退到miniredis用于开发/测试环境")
} else {
return nil, err
}
}
lc.Append(fx.Hook{
OnStop: func(ctx context.Context) error {
log.Info("正在关闭 Redis 连接")
return client.Close()
},
})
return client, nil
}
// provideStorage 创建对象存储客户端并注册关闭钩子。
// 存储服务在配置缺失时可能是可选的,失败时返回 error 让调用方决定是否降级。
func provideStorage(cfg *config.Config, lc fx.Lifecycle, log *zap.Logger) (*storage.StorageClient, error) {
client, err := storage.NewStorage(cfg.RustFS)
if err != nil {
return nil, err
}
// MinIO 客户端无需显式 Close但保留钩子以备未来扩展
lc.Append(fx.Hook{
OnStop: func(ctx context.Context) error {
log.Info("对象存储客户端已停止")
return nil
},
})
return client, nil
}

30
internal/app/module.go Normal file
View File

@@ -0,0 +1,30 @@
// Package app 提供 uber-go/fx 应用装配层。
//
// 该包用 fx 的 Provider/Lifecycle 取代了 pkg/*/manager.go 的全局单例和
// internal/container 的手动 DI是整个应用唯一的依赖装配入口。
//
// 使用方式:
//
// cfg, err := config.Load()
// if err != nil { log.Fatal(err) }
// fx.New(app.Module(cfg)).Run()
package app
import (
"carrotskin/pkg/config"
"go.uber.org/fx"
)
// Module 返回应用的根 fx.Option聚合所有子模块。
// cfg 通过 fx.Supply 注入,供各 Provider 消费其子配置结构。
//
// 当前阶段fx 管理 infra + Container 聚合 + 路由/服务器/任务生命周期。
// Container 内部构造 Repository 与 Service阶段6后将逐步解耦为 fx 直接管理 Service
func Module(cfg *config.Config) fx.Option {
return fx.Options(
fx.Supply(cfg),
InfraModule,
BootstrapModule,
)
}

View File

@@ -0,0 +1,13 @@
package app
// RepositoryModule 已在阶段4移除。
//
// 当前阶段Repository 由 internal/container.Container 内部构造。
//
// TODO(阶段6): Container 移除后Repository 将回到此 Module 由 fx 直接管理:
//
// var RepositoryModule = fx.Options(
// fx.Provide(repository.NewUserRepository),
// fx.Provide(repository.NewProfileRepository),
// ...
// )

View File

@@ -0,0 +1,7 @@
package app
// ServerModule 已在阶段4移除。
//
// 当前阶段HTTP 服务器生命周期由 container_module.go 的 registerServerLifecycle 管理。
//
// TODO(阶段6): 此处将作为独立 Module 重新启用。

View File

@@ -0,0 +1,15 @@
package app
// ServiceModule 已在阶段4移除。
//
// 当前阶段Service 由 internal/container.Container 内部构造Container 聚合了
// 所有 fx 注入的基础设施,在其 NewContainer 方法里创建 Repository 与 Service
//
// TODO(阶段6): 当 Handler 解耦为直接依赖各 Service 接口后Container 将被移除,
// 届时 Service 将回到此 Module 由 fx 直接管理:
//
// var ServiceModule = fx.Options(
// fx.Provide(service.NewUserService),
// fx.Provide(service.NewProfileService),
// ...
// )

View File

@@ -0,0 +1,7 @@
package app
// TaskModule 已在阶段4移除。
//
// 当前阶段,后台任务生命周期由 container_module.go 的 registerTaskLifecycle 管理。
//
// TODO(阶段6): 此处将作为独立 Module 重新启用。