2026-09-15 23:23:47 +03:30
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2026-09-19 22:17:12 +03:30
|
|
|
"context"
|
|
|
|
|
"errors"
|
2026-09-15 23:23:47 +03:30
|
|
|
"log"
|
2026-09-19 22:17:12 +03:30
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"os/signal"
|
|
|
|
|
"syscall"
|
|
|
|
|
"time"
|
2026-09-15 23:23:47 +03:30
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
|
|
|
|
"dating-app/internal/config"
|
|
|
|
|
"dating-app/internal/database"
|
|
|
|
|
"dating-app/internal/handler"
|
2026-09-19 22:17:12 +03:30
|
|
|
"dating-app/internal/middleware"
|
|
|
|
|
"dating-app/internal/monitoring"
|
2026-09-15 23:23:47 +03:30
|
|
|
"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)
|
2026-09-20 05:30:14 +03:30
|
|
|
reportRepo := repository.NewReportRepository(db)
|
2026-09-15 23:23:47 +03:30
|
|
|
|
|
|
|
|
// -------- 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,
|
|
|
|
|
)
|
2026-09-16 09:21:50 +03:30
|
|
|
onboardingService := service.NewOnboardingService(userRepo, optionRepo, questionRepo, photoRepo)
|
2026-09-15 23:23:47 +03:30
|
|
|
profileService := service.NewProfileService(photoRepo, userRepo, photoStorage)
|
2026-09-20 05:30:14 +03:30
|
|
|
adminService := service.NewAdminService(optionRepo, questionRepo, userRepo, reportRepo, matchRepo, photoRepo, profileService)
|
2026-09-15 23:23:47 +03:30
|
|
|
discoveryService := service.NewDiscoveryService(userRepo, photoRepo, swipeRepo, matchRepo, optionRepo, questionRepo)
|
|
|
|
|
chatService := service.NewChatService(matchRepo, messageRepo, userRepo, photoRepo)
|
2026-09-19 22:17:12 +03:30
|
|
|
backupService := service.NewBackupService(cfg, "./backups")
|
|
|
|
|
|
|
|
|
|
// -------- مانیتورینگ --------
|
|
|
|
|
metricsStore := monitoring.NewStore()
|
2026-09-15 23:23:47 +03:30
|
|
|
|
|
|
|
|
// -------- 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),
|
2026-09-19 22:17:12 +03:30
|
|
|
Monitoring: handler.NewMonitoringHandler(metricsStore),
|
|
|
|
|
Backup: handler.NewBackupHandler(backupService),
|
2026-09-15 23:23:47 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cfg.AppEnv == "production" {
|
|
|
|
|
gin.SetMode(gin.ReleaseMode)
|
|
|
|
|
}
|
2026-09-19 22:17:12 +03:30
|
|
|
// بهجای 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())
|
2026-09-15 23:23:47 +03:30
|
|
|
router.Setup(engine, handlers, jwtManager, userRepo)
|
|
|
|
|
|
2026-09-19 22:17:12 +03:30
|
|
|
// یک 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)
|
2026-09-15 23:23:47 +03:30
|
|
|
}
|
2026-09-19 22:17:12 +03:30
|
|
|
log.Println("سرور با موفقیت خاموش شد")
|
2026-09-15 23:23:47 +03:30
|
|
|
}
|