start rmser

This commit is contained in:
2025-11-29 08:40:24 +03:00
commit 5aa2238eea
2117 changed files with 375169 additions and 0 deletions

260
cmd/main.go Normal file
View File

@@ -0,0 +1,260 @@
package main
import (
"fmt"
"log"
"time"
"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"
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"
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)
syncService := sync.NewService(rmsClient, catalogRepo, recipesRepo, invoicesRepo, opsRepo)
recService := recServicePkg.NewService(recRepo)
ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, pyClient)
invoiceService := invServicePkg.NewService(rmsClient)
invoiceHandler := handlers.NewInvoiceHandler(invoiceService)
// --- БЛОК ПРОВЕРКИ СИНХРОНИЗАЦИИ (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()
api := r.Group("/api")
{
// ... другие роуты ...
api.POST("/invoices/send", invoiceHandler.SendInvoice)
}
// Простой хелсчек
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))
}
}