Files
rmser/cmd/main.go
SERTY ea1e5bbf6a 0302-добавил куки и сломал десктоп авторизацию.
сложно поддерживать однояйцевых близнецов - desktop и TMA, подготовил к рефакторингу структуры
2026-02-03 09:32:02 +03:00

287 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, accountRepo, 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)
// Новые endpoints для сессионной авторизации
api.POST("/auth/session", authHandler.CreateSession)
api.GET("/auth/me", authHandler.GetMe)
api.POST("/auth/logout", authHandler.Logout)
api.Use(middleware.AuthMiddleware(accountRepo, authService, 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))
}
}