Files
rmser/cmd/main.go
SERTY 10882f55c8 3001-фух.это был сильный спринт.
сервис стал сильно лучше
черновики сохраняются одним запросом
дто черновиков вынесен отдельно
2026-01-30 02:24:26 +03:00

244 lines
9.6 KiB
Go
Raw 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 (
"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"
"rmser/internal/transport/ws"
tgBot "rmser/internal/transport/telegram"
// 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"
// 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, accountRepo, catalogRepo, recipesRepo, invoicesRepo, opsRepo, supplierRepo)
recService := recServicePkg.NewService(recRepo)
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, 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)
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/: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.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))
}
}