From 714844058f10c81d6da08a8a8426a27e43ded907 Mon Sep 17 00:00:00 2001 From: SERTY Date: Sun, 30 Nov 2025 02:02:11 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=80=D1=83=D1=87=D0=BA=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=84?= =?UTF-8?q?=D1=80=D0=BE=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- cmd/main.go | 24 +++++++++++- go.mod | 5 ++- go.sum | 10 +++-- internal/domain/ocr/entity.go | 3 ++ .../infrastructure/repository/ocr/postgres.go | 8 ++++ internal/services/ocr/service.go | 6 ++- internal/transport/http/handlers/ocr.go | 12 ++++++ .../http/handlers/recommendations.go | 37 +++++++++++++++++++ 9 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 internal/transport/http/handlers/recommendations.go diff --git a/.gitignore b/.gitignore index 59421cf..a16197c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ # Python virtual environment -.venv \ No newline at end of file +.venv +*.py \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index a0afe6e..1f94fa3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,6 +5,7 @@ import ( "log" "time" + "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/shopspring/decimal" @@ -71,7 +72,11 @@ func main() { recService := recServicePkg.NewService(recRepo) ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, pyClient) invoiceService := invServicePkg.NewService(rmsClient) + + // --- Инициализация Handler'ов --- invoiceHandler := handlers.NewInvoiceHandler(invoiceService) + ocrHandler := handlers.NewOCRHandler(ocrService) + recommendHandler := handlers.NewRecommendationsHandler(recService) // --- БЛОК ПРОВЕРКИ СИНХРОНИЗАЦИИ (Run-once on start) --- go func() { @@ -129,16 +134,33 @@ func main() { } 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) + + // 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) } // Простой хелсчек diff --git a/go.mod b/go.mod index c2281b4..73834d6 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module rmser go 1.25.1 require ( + github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.11.0 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.6.0 @@ -22,13 +23,13 @@ require ( github.com/cloudwego/base64x v0.1.6 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect diff --git a/go.sum b/go.sum index c16c680..92a1541 100644 --- a/go.sum +++ b/go.sum @@ -132,9 +132,11 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= @@ -163,8 +165,8 @@ github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAu github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= diff --git a/internal/domain/ocr/entity.go b/internal/domain/ocr/entity.go index 407bf71..b88fc59 100644 --- a/internal/domain/ocr/entity.go +++ b/internal/domain/ocr/entity.go @@ -28,4 +28,7 @@ type Repository interface { // FindMatch ищет товар по точному совпадению названия FindMatch(rawName string) (*uuid.UUID, error) + + // GetAllMatches возвращает все существующие привязки + GetAllMatches() ([]ProductMatch, error) } diff --git a/internal/infrastructure/repository/ocr/postgres.go b/internal/infrastructure/repository/ocr/postgres.go index fee7ebb..635d344 100644 --- a/internal/infrastructure/repository/ocr/postgres.go +++ b/internal/infrastructure/repository/ocr/postgres.go @@ -47,3 +47,11 @@ func (r *pgRepository) FindMatch(rawName string) (*uuid.UUID, error) { return &match.ProductID, nil } + +func (r *pgRepository) GetAllMatches() ([]ocr.ProductMatch, error) { + var matches []ocr.ProductMatch + // Preload("Product") загружает связанную сущность товара, + // чтобы мы видели не только ID, но и название товара из каталога. + err := r.db.Preload("Product").Order("updated_at DESC").Find(&matches).Error + return matches, err +} diff --git a/internal/services/ocr/service.go b/internal/services/ocr/service.go index 1fd5b3d..9a4b384 100644 --- a/internal/services/ocr/service.go +++ b/internal/services/ocr/service.go @@ -67,7 +67,6 @@ func (s *Service) ProcessReceiptImage(ctx context.Context, imgData []byte) ([]Pr } else { // Если не нашли, пробуем найти точное совпадение по имени в каталоге (на всякий случай) // (В реальном проекте тут может быть нечеткий поиск, но пока точный) - // TODO: Добавить метод FindByName в репозиторий каталога, если нужно } processed = append(processed, item) @@ -118,6 +117,11 @@ func (s *Service) SaveMapping(rawName string, productID uuid.UUID) error { return s.ocrRepo.SaveMatch(rawName, productID) } +// GetKnownMatches возвращает список всех обученных связей +func (s *Service) GetKnownMatches() ([]ocr.ProductMatch, error) { + return s.ocrRepo.GetAllMatches() +} + // FindKnownMatch ищет, знаем ли мы уже этот товар func (s *Service) FindKnownMatch(rawName string) (*uuid.UUID, error) { return s.ocrRepo.FindMatch(rawName) diff --git a/internal/transport/http/handlers/ocr.go b/internal/transport/http/handlers/ocr.go index e87001c..d0a1204 100644 --- a/internal/transport/http/handlers/ocr.go +++ b/internal/transport/http/handlers/ocr.go @@ -57,3 +57,15 @@ func (h *OCRHandler) SaveMatch(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "saved"}) } + +// GetMatches возвращает список всех обученных связей +func (h *OCRHandler) GetMatches(c *gin.Context) { + matches, err := h.service.GetKnownMatches() + if err != nil { + logger.Log.Error("Ошибка получения списка матчей", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, matches) +} diff --git a/internal/transport/http/handlers/recommendations.go b/internal/transport/http/handlers/recommendations.go new file mode 100644 index 0000000..d7b9df7 --- /dev/null +++ b/internal/transport/http/handlers/recommendations.go @@ -0,0 +1,37 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" + + "rmser/internal/services/recommend" + "rmser/pkg/logger" +) + +type RecommendationsHandler struct { + service *recommend.Service +} + +func NewRecommendationsHandler(service *recommend.Service) *RecommendationsHandler { + return &RecommendationsHandler{service: service} +} + +// GetRecommendations godoc +// @Summary Получить список рекомендаций +// @Description Возвращает сгенерированные рекомендации (проблемные зоны учета) +// @Tags recommendations +// @Produce json +// @Success 200 {array} recommendations.Recommendation +// @Failure 500 {object} map[string]string +func (h *RecommendationsHandler) GetRecommendations(c *gin.Context) { + recs, err := h.service.GetRecommendations() + if err != nil { + logger.Log.Error("Ошибка получения рекомендаций", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, recs) +}