mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
302 lines
11 KiB
Go
302 lines
11 KiB
Go
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", draftsHandler.GetDrafts)
|
||
api.GET("/drafts/:id", draftsHandler.GetDraft)
|
||
api.DELETE("/drafts/:id", draftsHandler.DeleteDraft)
|
||
api.PATCH("/drafts/:id/items/:itemId", draftsHandler.UpdateItem)
|
||
api.POST("/drafts/:id/commit", draftsHandler.CommitDraft)
|
||
api.POST("/drafts/container", draftsHandler.AddContainer) // Добавление новой фасовки
|
||
|
||
// Склады
|
||
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)
|
||
api.GET("/ocr/search", ocrHandler.SearchProducts)
|
||
}
|
||
|
||
// Простой хелсчек
|
||
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))
|
||
}
|
||
}
|