Перевел на multi-tenant

Добавил поставщиков
Накладные успешно создаются из фронта
This commit is contained in:
2025-12-18 03:56:21 +03:00
parent 47ec8094e5
commit 542beafe0e
38 changed files with 1942 additions and 977 deletions

View File

@@ -1,24 +1,23 @@
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"
"rmser/internal/transport/http/middleware"
tgBot "rmser/internal/transport/telegram"
// Репозитории (инфраструктура)
// Repositories
accountPkg "rmser/internal/infrastructure/repository/account"
catalogPkg "rmser/internal/infrastructure/repository/catalog"
draftsPkg "rmser/internal/infrastructure/repository/drafts"
invoicesPkg "rmser/internal/infrastructure/repository/invoices"
@@ -26,43 +25,45 @@ import (
opsRepoPkg "rmser/internal/infrastructure/repository/operations"
recipesPkg "rmser/internal/infrastructure/repository/recipes"
recRepoPkg "rmser/internal/infrastructure/repository/recommendations"
suppliersPkg "rmser/internal/infrastructure/repository/suppliers"
"rmser/internal/infrastructure/rms"
// Services
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" // Хендлеры
// Handlers
"rmser/internal/transport/http/handlers"
"rmser/pkg/crypto"
"rmser/pkg/logger"
)
func main() {
// 1. Загрузка конфигурации
// 1. Config
cfg, err := config.LoadConfig(".")
if err != nil {
log.Fatalf("Ошибка загрузки конфига: %v", err)
}
// OCR Client
pyClient := ocr_client.NewClient(cfg.OCR.ServiceURL)
// 2. Инициализация логгера
// 2. Logger
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)
// 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. Инициализация слоев
rmsClient := rms.NewClient(cfg.RMS.BaseURL, cfg.RMS.Login, cfg.RMS.Password)
// 4. Repositories
accountRepo := accountPkg.NewRepository(database)
catalogRepo := catalogPkg.NewRepository(database)
recipesRepo := recipesPkg.NewRepository(database)
invoicesRepo := invoicesPkg.NewRepository(database)
@@ -70,124 +71,89 @@ func main() {
recRepo := recRepoPkg.NewRepository(database)
ocrRepo := ocrRepoPkg.NewRepository(database)
draftsRepo := draftsPkg.NewRepository(database)
supplierRepo := suppliersPkg.NewRepository(database)
syncService := sync.NewService(rmsClient, catalogRepo, recipesRepo, invoicesRepo, opsRepo)
// 5. RMS Factory
rmsFactory := rms.NewFactory(accountRepo, cryptoManager)
// 6. Services
pyClient := ocr_client.NewClient(cfg.OCR.ServiceURL)
syncService := sync.NewService(rmsFactory, accountRepo, catalogRepo, recipesRepo, invoicesRepo, opsRepo, supplierRepo)
recService := recServicePkg.NewService(recRepo)
ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, draftsRepo, pyClient)
draftsService := draftsServicePkg.NewService(draftsRepo, ocrRepo, catalogRepo, rmsClient)
invoiceService := invServicePkg.NewService(rmsClient)
ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, draftsRepo, accountRepo, pyClient)
draftsService := draftsServicePkg.NewService(draftsRepo, ocrRepo, catalogRepo, accountRepo, supplierRepo, rmsFactory)
// --- Инициализация Handler'ов ---
invoiceHandler := handlers.NewInvoiceHandler(invoiceService)
// 7. Handlers
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() блокирует поток)
// 8. Telegram Bot (Передаем syncService)
if cfg.Telegram.Token != "" {
bot, err := tgBot.NewBot(cfg.Telegram, ocrService)
// !!! syncService добавлен в аргументы
bot, err := tgBot.NewBot(cfg.Telegram, ocrService, syncService, accountRepo, rmsFactory, cryptoManager)
if err != nil {
logger.Log.Fatal("Ошибка создания Telegram бота", zap.Error(err))
}
go bot.Start()
defer bot.Stop() // Graceful shutdown
} else {
logger.Log.Warn("Telegram token не задан, бот не запущен")
defer bot.Stop()
}
// 5. Запуск HTTP сервера (Gin)
// 9. HTTP Server
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"}
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))
api := r.Group("/api")
{
// Invoices
api.POST("/invoices/send", invoiceHandler.SendInvoice)
// Черновики
api.Use(middleware.AuthMiddleware(accountRepo))
{
// Drafts & Invoices
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.POST("/drafts/container", draftsHandler.AddContainer)
// Склады
// Dictionaries
api.GET("/dictionaries", draftsHandler.GetDictionaries)
api.GET("/dictionaries/stores", draftsHandler.GetStores)
// Recommendations
api.GET("/recommendations", recommendHandler.GetRecommendations)
// OCR
// 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.GET("/ocr/search", ocrHandler.SearchProducts)
// Manual Sync Trigger
api.POST("/sync/all", func(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
// Запускаем в горутине, чтобы не держать соединение
go func() {
if err := syncService.SyncAllData(userID); 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),
})
c.JSON(200, gin.H{"status": "ok", "time": time.Now().Format(time.RFC3339)})
})
logger.Log.Info("Сервер запускается", zap.String("port", cfg.App.Port))
@@ -195,107 +161,3 @@ func main() {
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))
}
}