package main import ( "context" "errors" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" "dating-app/internal/config" "dating-app/internal/database" "dating-app/internal/handler" "dating-app/internal/middleware" "dating-app/internal/monitoring" "dating-app/internal/repository" "dating-app/internal/router" "dating-app/internal/service" "dating-app/internal/utils" ) func main() { cfg := config.Load() db, err := database.Connect(cfg) if err != nil { log.Fatalf("اتصال به دیتابیس ناموفق بود: %v", err) } if err := database.AutoMigrate(db); err != nil { log.Fatalf("migrate ناموفق بود: %v", err) } if err := database.SeedCategories(db); err != nil { log.Fatalf("seed کردن دسته‌ها ناموفق بود: %v", err) } if err := database.BootstrapAdmin(db, cfg.AdminBootstrapPhone); err != nil { log.Fatalf("ساخت ادمین اولیه ناموفق بود: %v", err) } // -------- Repositories -------- userRepo := repository.NewUserRepository(db) optionRepo := repository.NewOptionRepository(db) questionRepo := repository.NewQuestionRepository(db) photoRepo := repository.NewPhotoRepository(db) swipeRepo := repository.NewSwipeRepository(db) matchRepo := repository.NewMatchRepository(db) messageRepo := repository.NewMessageRepository(db) // -------- Utils / Infra -------- jwtManager := utils.NewJWTManager( cfg.JWTAccessSecret, cfg.JWTRefreshSecret, cfg.JWTAccessTTL, cfg.JWTRefreshTTL, ) // برای اتصال به یک گیت‌وی پیامک واقعی، این خط رو با پیاده‌سازی خودت // (که service.OTPSender رو implement می‌کنه) عوض کن. otpSender := service.NewMockOTPSender(cfg.AppEnv != "production") // برای اتصال به یک object storage واقعی، این خط رو با پیاده‌سازی خودت // (که service.PhotoStorage رو implement می‌کنه) عوض کن. photoStorage := service.NewLocalPhotoStorage("./uploads/photos", "/uploads/photos") // -------- Services -------- authService := service.NewAuthService( userRepo, otpSender, jwtManager, cfg.OTPExpirySeconds, cfg.OTPLength, cfg.OTPHashSecret, ) onboardingService := service.NewOnboardingService(userRepo, optionRepo, questionRepo, photoRepo) adminService := service.NewAdminService(optionRepo, questionRepo, userRepo) profileService := service.NewProfileService(photoRepo, userRepo, photoStorage) discoveryService := service.NewDiscoveryService(userRepo, photoRepo, swipeRepo, matchRepo, optionRepo, questionRepo) chatService := service.NewChatService(matchRepo, messageRepo, userRepo, photoRepo) backupService := service.NewBackupService(cfg, "./backups") // -------- مانیتورینگ -------- metricsStore := monitoring.NewStore() // -------- Handlers -------- handlers := router.Handlers{ Auth: handler.NewAuthHandler(authService), Onboarding: handler.NewOnboardingHandler(onboardingService), Profile: handler.NewProfileHandler(optionRepo, questionRepo, photoRepo, onboardingService, profileService), Admin: handler.NewAdminHandler(adminService, optionRepo, questionRepo, photoRepo, onboardingService), Discovery: handler.NewDiscoveryHandler(discoveryService), Chat: handler.NewChatHandler(chatService), Monitoring: handler.NewMonitoringHandler(metricsStore), Backup: handler.NewBackupHandler(backupService), } if cfg.AppEnv == "production" { gin.SetMode(gin.ReleaseMode) } // به‌جای gin.Default() (که فقط Logger+Recovery پیش‌فرض رو می‌ده)، دستی // می‌سازیم تا هم Metrics رو اضافه کنیم و هم Recovery خودمون که envelope // استاندارد JSON برمی‌گردونه. ترتیب مهمه: Metrics باید بیرونی‌ترین لایه // باشه تا حتی درخواست‌هایی که پنیک می‌کنن (و توسط Recovery مهار می‌شن) هم // توی آمار لحاظ بشن. engine := gin.New() engine.Use(middleware.Metrics(metricsStore)) engine.Use(middleware.Recovery()) engine.Use(gin.Logger()) router.Setup(engine, handlers, jwtManager, userRepo) // یک http.Server دستی به‌جای engine.Run() ساده، به دو دلیل: // ۱. تایم‌اوت‌های صریح روی خوندن/نوشتن، تا یک کلاینت کند یا مخرب (slowloris) // نتونه goroutine/فایل‌دسکریپتورهای سرور رو زیر بار بالا قفل نگه داره. // ۲. Shutdown مهلت‌دار، تا موقع دیپلوی/ری‌استارت، درخواست‌های در حال پردازش // ناتمام قطع نشن (به‌جای کشتن فوری پروسه). srv := &http.Server{ Addr: ":" + cfg.AppPort, Handler: engine, ReadTimeout: 30 * time.Second, ReadHeaderTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 90 * time.Second, } go func() { log.Printf("سرور روی پورت %s در حال اجراست (env=%s)", cfg.AppPort, cfg.AppEnv) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("اجرای سرور ناموفق بود: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("سیگنال خاموشی دریافت شد؛ منتظر تمام‌شدن درخواست‌های در حال پردازش...") ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatalf("خاموش کردن سرور با مشکل مواجه شد: %v", err) } log.Println("سرور با موفقیت خاموش شد") }