Refactor: Implement uber-go/fx dependency injection

- Replace global variable pattern with proper dependency injection
- Add uber-go/fx for automatic dependency resolution
- Refactor all services and handlers to use constructor injection
- Eliminate fragile initialization order dependencies
- Improve testability and modularity
- Add structured logging with zap

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2025-09-16 11:41:01 +03:30
co-authored by Qwen-Coder
parent d4044b0eaf
commit f136ae58b3
15 changed files with 389 additions and 255 deletions
+123 -64
View File
@@ -1,75 +1,134 @@
package main
import (
"context"
"time"
"CatsOfMastodonBotGo/internal/auth"
"CatsOfMastodonBotGo/internal/config"
"CatsOfMastodonBotGo/internal/database"
"CatsOfMastodonBotGo/internal/domain"
"CatsOfMastodonBotGo/internal/server"
"CatsOfMastodonBotGo/internal/services"
"context"
"log/slog"
"strconv"
"time"
"CatsOfMastodonBotGo/internal/web/handlers"
"go.uber.org/fx"
"go.uber.org/zap"
)
func main() {
// Setup config
config.Init()
// Initialize database
database.Init()
services.InitPostService()
// Not needed but anyways
services.InitImgKitHelper()
ticker := time.NewTicker(10 * time.Minute)
runFetchPosts := func() {
// Get posts
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
var posts []domain.Post = nil
err, posts := services.PostServiceInstance.GetPostsFromApi(ctx, config.Config.Tag, config.Config.Instance)
if err != nil {
slog.Error(err.Error())
return
}
var existingPostIds = services.PostServiceInstance.GetExistingPostIds()
var existingAccountIds = services.PostServiceInstance.GetExistingAccountIds()
var newPosts = services.PostServiceInstance.GetNewPosts(existingPostIds, posts)
var newAccounts = services.PostServiceInstance.GetNewAccounts(existingAccountIds, newPosts)
// Save to database
slog.Info("Fetched " + strconv.Itoa(len(posts)) + " posts; " + strconv.Itoa(len(existingPostIds)) + " existing posts; " + strconv.Itoa(len(newPosts)) + " new posts and " + strconv.Itoa(len(newAccounts)) + " new accounts\n")
// Additional logging
if newAccounts != nil {
slog.Info("Inserted " + strconv.Itoa(services.PostServiceInstance.InsertNewAccounts(newAccounts)) + " accounts\n")
}
if newPosts != nil {
slog.Info("Inserted " + strconv.Itoa(services.PostServiceInstance.InsertNewPosts(newPosts)) + " posts\n")
}
}
go func() {
for range ticker.C {
runFetchPosts()
}
}()
// Run initial fetch on startup
go func() {
runFetchPosts()
}()
// https://seefnasrul.medium.com/create-your-first-go-rest-api-with-jwt-authentication-in-gin-framework-dbe5bda72817
r := server.SetupRouter()
err := r.Run(":8080")
if err != nil {
slog.Error(err.Error())
}
fx.New(
fx.Provide(
// Configuration
config.Load,
// Database
database.Connect,
// Services
services.NewPostService,
services.NewImgKitHelper,
// Auth
auth.NewJwtTokenGenerator,
auth.NewGiteaOauth2Token,
// Handlers
handlers.NewAdminDashboardHandler,
handlers.NewApiEndpointHandler,
handlers.NewEmbedCardHandler,
handlers.NewOauthLoginHandler,
// Logger
NewLogger,
),
fx.Invoke(
// Start background tasks
startBackgroundTasks,
// Setup and start server
server.SetupRouter,
),
fx.Logger(NewFXLogger()), // Optional custom logger
).Run()
}
// Background tasks as a separate function
func startBackgroundTasks(
lc fx.Lifecycle,
postService *services.PostService,
cfg *config.Config,
logger *zap.Logger,
) {
lc.Append(fx.Hook{
OnStart: func(context.Context) error {
ticker := time.NewTicker(10 * time.Minute)
runFetchPosts := func() {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
posts, err := postService.GetPostsFromApi(ctx, cfg.Tag, cfg.Instance)
if err != nil {
logger.Error("Failed to fetch posts", zap.Error(err))
return
}
existingPostIds := postService.GetExistingPostIds()
existingAccountIds := postService.GetExistingAccountIds()
newPosts := postService.GetNewPosts(existingPostIds, posts)
newAccounts := postService.GetNewAccounts(existingAccountIds, newPosts)
logger.Info("Fetched posts",
zap.Int("total", len(posts)),
zap.Int("existing", len(existingPostIds)),
zap.Int("new", len(newPosts)),
zap.Int("new_accounts", len(newAccounts)))
if len(newAccounts) > 0 {
count := postService.InsertNewAccounts(newAccounts)
logger.Info("Inserted accounts", zap.Int("count", count))
}
if len(newPosts) > 0 {
count := postService.InsertNewPosts(newPosts)
logger.Info("Inserted posts", zap.Int("count", count))
}
}
// Run initial fetch
go runFetchPosts()
// Start ticker
go func() {
for range ticker.C {
runFetchPosts()
}
}()
return nil
},
OnStop: func(context.Context) error {
// Cleanup if needed
return nil
},
})
}
// Logger provider
func NewLogger() (*zap.Logger, error) {
return zap.NewDevelopment()
}
// Simple logger for fx
type FXLogger struct {
logger *zap.Logger
}
func NewFXLogger() *FXLogger {
logger, _ := zap.NewDevelopment()
return &FXLogger{logger: logger}
}
func (l *FXLogger) Printf(str string, args ...interface{}) {
l.logger.Sugar().Infof(str, args...)
}