Files
rmser/cmd/main.go

298 lines
11 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 (
"fmt"
"log"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"rmser/config"
"rmser/internal/domain/catalog"
"rmser/internal/domain/invoices"
"rmser/internal/infrastructure/db"
"rmser/internal/infrastructure/ocr_client"
tgBot "rmser/internal/transport/telegram"
// Репозитории (инфраструктура)
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"
recipesPkg "rmser/internal/infrastructure/repository/recipes"
recRepoPkg "rmser/internal/infrastructure/repository/recommendations"
"rmser/internal/infrastructure/rms"
draftsServicePkg "rmser/internal/services/drafts"
invServicePkg "rmser/internal/services/invoices" // Сервис накладных
ocrServicePkg "rmser/internal/services/ocr"
recServicePkg "rmser/internal/services/recommend"
"rmser/internal/services/sync"
"rmser/internal/transport/http/handlers" // Хендлеры
"rmser/pkg/logger"
)
func main() {
// 1. Загрузка конфигурации
cfg, err := config.LoadConfig(".")
if err != nil {
log.Fatalf("Ошибка загрузки конфига: %v", err)
}
// OCR Client
pyClient := ocr_client.NewClient(cfg.OCR.ServiceURL)
// 2. Инициализация логгера
logger.Init(cfg.App.Mode)
defer logger.Log.Sync()
logger.Log.Info("Запуск приложения rmser", zap.String("mode", cfg.App.Mode))
// 3a. Подключение Redis (Новое)
// redisClient, err := redis.NewClient(cfg.Redis.Addr, cfg.Redis.Password, cfg.Redis.DB)
// if err != nil {
// logger.Log.Fatal("Ошибка подключения к Redis", zap.Error(err))
// }
// 3. Подключение к БД (PostgreSQL)
database := db.NewPostgresDB(cfg.DB.DSN)
// 4. Инициализация слоев
rmsClient := rms.NewClient(cfg.RMS.BaseURL, cfg.RMS.Login, cfg.RMS.Password)
catalogRepo := catalogPkg.NewRepository(database)
recipesRepo := recipesPkg.NewRepository(database)
invoicesRepo := invoicesPkg.NewRepository(database)
opsRepo := opsRepoPkg.NewRepository(database)
recRepo := recRepoPkg.NewRepository(database)
ocrRepo := ocrRepoPkg.NewRepository(database)
draftsRepo := draftsPkg.NewRepository(database)
syncService := sync.NewService(rmsClient, catalogRepo, recipesRepo, invoicesRepo, opsRepo)
recService := recServicePkg.NewService(recRepo)
ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, draftsRepo, pyClient)
draftsService := draftsServicePkg.NewService(draftsRepo, ocrRepo, catalogRepo, rmsClient)
invoiceService := invServicePkg.NewService(rmsClient)
// --- Инициализация Handler'ов ---
invoiceHandler := handlers.NewInvoiceHandler(invoiceService)
draftsHandler := handlers.NewDraftsHandler(draftsService)
ocrHandler := handlers.NewOCRHandler(ocrService)
recommendHandler := handlers.NewRecommendationsHandler(recService)
// --- БЛОК ПРОВЕРКИ СИНХРОНИЗАЦИИ (Run-once on start) ---
go func() {
logger.Log.Info(">>> Запуск тестовой синхронизации...")
// 1. Каталог
if err := syncService.SyncCatalog(); err != nil {
logger.Log.Error("Ошибка синхронизации каталога", zap.Error(err))
} else {
logger.Log.Info("<<< Каталог успешно синхронизирован")
}
// 2. Техкарты
if err := syncService.SyncRecipes(); err != nil {
logger.Log.Error("Ошибка синхронизации техкарт", zap.Error(err))
} else {
logger.Log.Info("<<< Техкарты успешно синхронизированы")
}
// 3. Накладные
if err := syncService.SyncInvoices(); err != nil {
logger.Log.Error("Ошибка синхронизации накладных", zap.Error(err))
} else {
logger.Log.Info("<<< Накладные успешно синхронизированы")
}
// 4. Складские операции
if err := syncService.SyncStoreOperations(); err != nil {
logger.Log.Error("Ошибка синхронизации операций", zap.Error(err))
} else {
logger.Log.Info("<<< Операции успешно синхронизированы")
}
logger.Log.Info(">>> Запуск расчета рекомендаций...")
if err := recService.RefreshRecommendations(); err != nil {
logger.Log.Error("Ошибка расчета рекомендаций", zap.Error(err))
} else {
// Для отладки можно вывести пару штук
recs, _ := recService.GetRecommendations()
logger.Log.Info("<<< Анализ завершен", zap.Int("found", len(recs)))
}
// === ТЕСТ ОТПРАВКИ НАКЛАДНОЙ ===
// Запускаем через небольшую паузу, чтобы логи не перемешались
// time.Sleep(2 * time.Second)
// runManualInvoiceTest(rmsClient, catalogRepo)
// ===============================
}()
// -------------------------------------------------------
// Запуск бота (в отдельной горутине, т.к. Start() блокирует поток)
if cfg.Telegram.Token != "" {
bot, err := tgBot.NewBot(cfg.Telegram, ocrService)
if err != nil {
logger.Log.Fatal("Ошибка создания Telegram бота", zap.Error(err))
}
go bot.Start()
defer bot.Stop() // Graceful shutdown
} else {
logger.Log.Warn("Telegram token не задан, бот не запущен")
}
// 5. Запуск HTTP сервера (Gin)
if cfg.App.Mode == "release" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.Default()
// --- Настройка CORS ---
// Разрешаем запросы с любых источников для разработки Frontend
corsConfig := cors.DefaultConfig()
corsConfig.AllowAllOrigins = true // В продакшене заменить на AllowOrigins: []string{"http://domain.com"}
corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}
corsConfig.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization"}
r.Use(cors.New(corsConfig))
api := r.Group("/api")
{
// Invoices
api.POST("/invoices/send", invoiceHandler.SendInvoice)
// Черновики
api.GET("/drafts/:id", draftsHandler.GetDraft)
api.PATCH("/drafts/:id/items/:itemId", draftsHandler.UpdateItem)
api.POST("/drafts/:id/commit", draftsHandler.CommitDraft)
// Склады
api.GET("/dictionaries/stores", draftsHandler.GetStores)
// Recommendations
api.GET("/recommendations", recommendHandler.GetRecommendations)
// OCR
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)
}
// Простой хелсчек
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))
}
}
// runManualInvoiceTest создает тестовую накладную и отправляет её в RMS
func runManualInvoiceTest(client rms.ClientI, catRepo catalog.Repository) {
logger.Log.Info(">>> [TEST] Начало теста создания накладной...")
// === НАСТРОЙКИ ТЕСТА ===
const (
MyStoreGUID = "1239d270-1bbe-f64f-b7ea-5f00518ef508" // <-- ВАШ СКЛАД
MySupplierGUID = "780aa87e-2688-4f99-915b-7924ca392ac1" // <-- ВАШ ПОСТАВЩИК
MyProductGUID = "607a1e96-f539-45d2-8709-3919f94bdc3e" // <-- ВАШ ТОВАР (Опционально)
)
// =======================
// 1. Поиск товара
var targetProduct catalog.Product
var found bool
// Если задан конкретный ID товара в константе - ищем его
if MyProductGUID != "00000000-0000-0000-0000-000000000000" {
products, _ := catRepo.GetAll() // В реальном коде лучше GetByID, но у нас repo пока простой
targetUUID := uuid.MustParse(MyProductGUID)
for _, p := range products {
if p.ID == targetUUID {
targetProduct = p
found = true
break
}
}
if !found {
logger.Log.Error("[TEST] Товар с указанным MyProductGUID не найден в локальной БД")
return
}
} else {
// Иначе ищем первый попавшийся GOODS
products, err := catRepo.GetAll()
if err != nil || len(products) == 0 {
logger.Log.Error("[TEST] БД пуста. Выполните SyncCatalog.")
return
}
for _, p := range products {
// Ищем именно товар (GOODS) или заготовку (PREPARED), но не группу и не блюдо, если нужно
if p.Type == "GOODS" {
targetProduct = p
found = true
break
}
}
if !found {
logger.Log.Warn("[TEST] Не найдено товаров с типом GOODS. Берем первый попавшийся.")
targetProduct = products[0]
}
}
logger.Log.Info("[TEST] Используем товар",
zap.String("name", targetProduct.Name),
zap.String("type", targetProduct.Type),
zap.String("uuid", targetProduct.ID.String()),
)
// 2. Парсинг ID Склада и Поставщика
storeID := uuid.Nil
if MyStoreGUID != "00000000-0000-0000-0000-000000000000" {
storeID = uuid.MustParse(MyStoreGUID)
} else {
logger.Log.Warn("[TEST] ID склада не задан! iiko вернет ошибку валидации.")
storeID = uuid.New() // Рандом, чтобы XML собрался
}
supplierID := uuid.Nil
if MySupplierGUID != "00000000-0000-0000-0000-000000000000" {
supplierID = uuid.MustParse(MySupplierGUID)
} else {
logger.Log.Warn("[TEST] ID поставщика не задан!")
supplierID = uuid.New()
}
// 3. Формируем накладную
testInv := invoices.Invoice{
ID: uuid.Nil,
DocumentNumber: fmt.Sprintf("TEST-%d", time.Now().Unix()%10000),
DateIncoming: time.Now(),
SupplierID: supplierID,
DefaultStoreID: storeID,
Status: "NEW",
Items: []invoices.InvoiceItem{
{
ProductID: targetProduct.ID,
Amount: decimal.NewFromFloat(5.0),
Price: decimal.NewFromFloat(100.0),
Sum: decimal.NewFromFloat(500.0),
},
},
}
// 4. Отправляем
logger.Log.Info("[TEST] Отправка запроса в RMS...")
docNum, err := client.CreateIncomingInvoice(testInv)
if err != nil {
logger.Log.Error("[TEST] Ошибка отправки накладной", zap.Error(err))
} else {
logger.Log.Info("[TEST] УСПЕХ! Накладная создана.", zap.String("doc_number", docNum))
}
}