package main import ( "context" "log" "os" "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" "rmser/config" "rmser/internal/infrastructure/db" "rmser/internal/infrastructure/ocr_client" "rmser/internal/infrastructure/yookassa" "rmser/internal/services/auth" "rmser/internal/transport/http/middleware" tgBot "rmser/internal/transport/telegram" "rmser/internal/transport/ws" // Repositories accountPkg "rmser/internal/infrastructure/repository/account" billingPkg "rmser/internal/infrastructure/repository/billing" catalogPkg "rmser/internal/infrastructure/repository/catalog" draftsPkg "rmser/internal/infrastructure/repository/drafts" invoicesPkg "rmser/internal/infrastructure/repository/invoices" ocrRepoPkg "rmser/internal/infrastructure/repository/ocr" opsRepoPkg "rmser/internal/infrastructure/repository/operations" photosPkg "rmser/internal/infrastructure/repository/photos" recipesPkg "rmser/internal/infrastructure/repository/recipes" recRepoPkg "rmser/internal/infrastructure/repository/recommendations" suppliersPkg "rmser/internal/infrastructure/repository/suppliers" "rmser/internal/infrastructure/rms" // Services billingServicePkg "rmser/internal/services/billing" draftsServicePkg "rmser/internal/services/drafts" invoicesServicePkg "rmser/internal/services/invoices" ocrServicePkg "rmser/internal/services/ocr" photosServicePkg "rmser/internal/services/photos" recServicePkg "rmser/internal/services/recommend" "rmser/internal/services/sync" "rmser/internal/services/worker" // Handlers "rmser/internal/transport/http/handlers" "rmser/pkg/crypto" "rmser/pkg/logger" ) func main() { // 1. Config cfg, err := config.LoadConfig(".") if err != nil { log.Fatalf("Ошибка загрузки конфига: %v", err) } // Проверяем, что bot_username задан в конфиге if cfg.Telegram.BotUsername == "" { log.Fatalf("Telegram.BotUsername не задан в конфиге! Это обязательное поле для авторизации.") } // 2. Logger logger.Init(cfg.App.Mode) defer logger.Log.Sync() if err := os.MkdirAll(cfg.App.StoragePath, 0755); err != nil { logger.Log.Fatal("Не удалось создать директорию для загрузок", zap.Error(err), zap.String("path", cfg.App.StoragePath)) } logger.Log.Info("Запуск приложения rmser", zap.String("mode", cfg.App.Mode)) // 3. Crypto & DB if cfg.Security.SecretKey == "" { logger.Log.Fatal("Security.SecretKey не задан в конфиге!") } cryptoManager := crypto.NewCryptoManager(cfg.Security.SecretKey) database := db.NewPostgresDB(cfg.DB.DSN) // 4. Repositories accountRepo := accountPkg.NewRepository(database) billingRepo := billingPkg.NewRepository(database) catalogRepo := catalogPkg.NewRepository(database) recipesRepo := recipesPkg.NewRepository(database) invoicesRepo := invoicesPkg.NewRepository(database) opsRepo := opsRepoPkg.NewRepository(database) recRepo := recRepoPkg.NewRepository(database) ocrRepo := ocrRepoPkg.NewRepository(database) photosRepo := photosPkg.NewRepository(database) draftsRepo := draftsPkg.NewRepository(database) supplierRepo := suppliersPkg.NewRepository(database) // 5. RMS Factory rmsFactory := rms.NewFactory(accountRepo, cryptoManager) // 6. Services pyClient := ocr_client.NewClient(cfg.OCR.ServiceURL) ykClient := yookassa.NewClient(cfg.YooKassa.ShopID, cfg.YooKassa.SecretKey) billingService := billingServicePkg.NewService(billingRepo, accountRepo, ykClient) syncService := sync.NewService(rmsFactory, cryptoManager, accountRepo, catalogRepo, recipesRepo, invoicesRepo, opsRepo, supplierRepo) // Создаем сервис рекомендаций до SyncWorker, так как он нужен для обновления рекомендаций recService := recServicePkg.NewService(recRepo) // 6.1 SyncWorker для фоновой синхронизации syncWorker := worker.NewSyncWorker(syncService, accountRepo, rmsFactory, recService, logger.Log) workerCtx, workerCancel := context.WithCancel(context.Background()) go syncWorker.Run(workerCtx) defer workerCancel() ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, draftsRepo, accountRepo, photosRepo, pyClient, cfg.App.StoragePath) // Устанавливаем DevIDs для OCR сервиса ocrService.SetDevIDs(cfg.App.DevIDs) draftsService := draftsServicePkg.NewService(draftsRepo, ocrRepo, catalogRepo, accountRepo, supplierRepo, photosRepo, invoicesRepo, rmsFactory, billingService) invoicesService := invoicesServicePkg.NewService(invoicesRepo, draftsRepo, supplierRepo, accountRepo, rmsFactory) photosService := photosServicePkg.NewService(photosRepo, draftsRepo, accountRepo) // 7. WebSocket сервер для desktop авторизации wsServer := ws.NewServer() go wsServer.Run() // 8. Сервис авторизации для desktop auth authService := auth.NewService(accountRepo, wsServer, cfg.Security.SecretKey) // 9. Handlers draftsHandler := handlers.NewDraftsHandler(draftsService, ocrService) billingHandler := handlers.NewBillingHandler(billingService) ocrHandler := handlers.NewOCRHandler(ocrService) photosHandler := handlers.NewPhotosHandler(photosService) recommendHandler := handlers.NewRecommendationsHandler(recService, accountRepo) settingsHandler := handlers.NewSettingsHandler(accountRepo, catalogRepo) settingsHandler.SetRMSFactory(rmsFactory) invoicesHandler := handlers.NewInvoiceHandler(invoicesService, syncService) authHandler := handlers.NewAuthHandler(authService, cfg.Telegram.BotUsername) // 10. Telegram Bot (Передаем syncService и authService) if cfg.Telegram.Token != "" { bot, err := tgBot.NewBot(cfg.Telegram, ocrService, syncService, billingService, accountRepo, rmsFactory, cryptoManager, draftsService, authService, cfg.App.MaintenanceMode, cfg.App.DevIDs) if err != nil { logger.Log.Fatal("Ошибка создания Telegram бота", zap.Error(err)) } billingService.SetNotifier(bot) settingsHandler.SetNotifier(bot) // Устанавливаем нотификатор для OCR сервиса ocrService.SetNotifier(bot) go bot.Start() defer bot.Stop() } // 11. HTTP Server if cfg.App.Mode == "release" { gin.SetMode(gin.ReleaseMode) } r := gin.Default() // Регистрируем WebSocket хендлер r.GET("/socket.io/", wsServer.HandleConnections) r.POST("/api/webhooks/yookassa", billingHandler.YooKassaWebhook) corsConfig := cors.DefaultConfig() corsConfig.AllowAllOrigins = true corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"} corsConfig.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization", "X-Telegram-User-ID"} r.Use(cors.New(corsConfig)) // --- STATIC FILES SERVING --- // Раздаем папку uploads по урлу /api/uploads r.Static("/api/uploads", cfg.App.StoragePath) api := r.Group("/api") // Хендлер инициализации desktop авторизации (без middleware) api.POST("/auth/init-desktop", authHandler.InitDesktopAuth) api.Use(middleware.AuthMiddleware(accountRepo, cfg.Telegram.Token, cfg.Security.SecretKey, cfg.App.MaintenanceMode, cfg.App.DevIDs)) { // Drafts & Invoices api.GET("/drafts", draftsHandler.GetDrafts) api.GET("/drafts/:id", draftsHandler.GetDraft) api.PATCH("/drafts/:id", draftsHandler.UpdateDraft) api.DELETE("/drafts/:id", draftsHandler.DeleteDraft) api.POST("/drafts/upload", draftsHandler.Upload) // Items CRUD api.POST("/drafts/:id/items", draftsHandler.AddDraftItem) api.DELETE("/drafts/:id/items/:itemId", draftsHandler.DeleteDraftItem) api.PATCH("/drafts/:id/items/:itemId", draftsHandler.UpdateItem) api.POST("/drafts/:id/reorder", draftsHandler.ReorderItems) api.POST("/drafts/:id/commit", draftsHandler.CommitDraft) api.POST("/drafts/container", draftsHandler.AddContainer) // Settings api.GET("/settings", settingsHandler.GetSettings) api.POST("/settings", settingsHandler.UpdateSettings) // User Servers api.GET("/user/servers", settingsHandler.GetUserServers) api.POST("/user/servers/active", settingsHandler.SwitchActiveServer) // Photos Storage api.GET("/photos", photosHandler.GetPhotos) api.DELETE("/photos/:id", photosHandler.DeletePhoto) // User Management api.GET("/settings/users", settingsHandler.GetServerUsers) api.PATCH("/settings/users/:userId", settingsHandler.UpdateUserRole) api.DELETE("/settings/users/:userId", settingsHandler.RemoveUser) // Dictionaries api.GET("/dictionaries", draftsHandler.GetDictionaries) api.GET("/dictionaries/groups", settingsHandler.GetGroupsTree) api.GET("/dictionaries/stores", draftsHandler.GetStores) // Recommendations api.GET("/recommendations", recommendHandler.GetRecommendations) // OCR & Matching api.GET("/ocr/catalog", ocrHandler.GetCatalog) api.GET("/ocr/matches", ocrHandler.GetMatches) api.POST("/ocr/match", ocrHandler.SaveMatch) api.DELETE("/ocr/match", ocrHandler.DeleteMatch) api.GET("/ocr/unmatched", ocrHandler.GetUnmatched) api.DELETE("/ocr/unmatched", ocrHandler.DiscardUnmatched) api.GET("/ocr/search", ocrHandler.SearchProducts) // Invoices api.GET("/invoices/stats", invoicesHandler.GetStats) api.GET("/invoices/:id", invoicesHandler.GetInvoice) api.POST("/invoices/sync", invoicesHandler.SyncInvoices) // Manual Sync Trigger api.POST("/sync/all", func(c *gin.Context) { userID := c.MustGet("userID").(uuid.UUID) force := c.Query("force") == "true" // Запускаем в горутине, чтобы не держать соединение go func() { if err := syncService.SyncAllData(userID, force); err != nil { logger.Log.Error("Manual sync failed", zap.String("user_id", userID.String()), zap.Error(err)) return } // Обновляем рекомендации после успешной синхронизации // Получаем активный сервер пользователя server, err := accountRepo.GetActiveServer(userID) if err != nil { logger.Log.Error("Ошибка получения активного сервера для обновления рекомендаций", zap.String("user_id", userID.String()), zap.Error(err)) return } if server != nil { if err := recService.RefreshRecommendations(server.ID); err != nil { logger.Log.Error("Ошибка обновления рекомендаций после ручной синхронизации", zap.String("user_id", userID.String()), zap.String("server_id", server.ID.String()), zap.Error(err)) } if err := accountRepo.UpdateLastSync(server.ID); err != nil { logger.Log.Error("Ошибка обновления времени синхронизации", zap.String("user_id", userID.String()), zap.String("server_id", server.ID.String()), zap.Error(err)) } } }() c.JSON(200, gin.H{"status": "sync_started", "message": "Синхронизация запущена в фоне"}) }) } r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok", "time": time.Now().Format(time.RFC3339)}) }) logger.Log.Info("Сервер запускается", zap.String("port", cfg.App.Port)) if err := r.Run(":" + cfg.App.Port); err != nil { logger.Log.Fatal("Ошибка запуска сервера", zap.Error(err)) } }