package ocr import ( "context" "fmt" "strings" "github.com/google/uuid" "github.com/shopspring/decimal" "go.uber.org/zap" "rmser/internal/domain/catalog" "rmser/internal/domain/ocr" "rmser/internal/infrastructure/ocr_client" "rmser/pkg/logger" ) type Service struct { ocrRepo ocr.Repository catalogRepo catalog.Repository pyClient *ocr_client.Client // Клиент к Python сервису } func NewService( ocrRepo ocr.Repository, catalogRepo catalog.Repository, pyClient *ocr_client.Client, ) *Service { return &Service{ ocrRepo: ocrRepo, catalogRepo: catalogRepo, pyClient: pyClient, } } // ProcessReceiptImage - основной метод: Картинка -> Распознавание -> Матчинг func (s *Service) ProcessReceiptImage(ctx context.Context, imgData []byte) ([]ProcessedItem, error) { // 1. Отправляем в Python rawResult, err := s.pyClient.ProcessImage(ctx, imgData, "receipt.jpg") if err != nil { return nil, fmt.Errorf("python ocr error: %w", err) } var processed []ProcessedItem for _, rawItem := range rawResult.Items { item := ProcessedItem{ RawName: rawItem.RawName, Amount: decimal.NewFromFloat(rawItem.Amount), Price: decimal.NewFromFloat(rawItem.Price), Sum: decimal.NewFromFloat(rawItem.Sum), } match, err := s.ocrRepo.FindMatch(rawItem.RawName) if err != nil { logger.Log.Error("db error finding match", zap.Error(err)) } if match != nil { item.ProductID = &match.ProductID item.IsMatched = true item.MatchSource = "learned" // Здесь мы могли бы подтянуть quantity/container из матча, // но пока фронт сам это сделает, запросив /ocr/matches или получив подсказку. } else { if err := s.ocrRepo.UpsertUnmatched(rawItem.RawName); err != nil { logger.Log.Warn("failed to save unmatched", zap.Error(err)) } } processed = append(processed, item) } return processed, nil } // ProcessedItem - результат обработки одной строки чека type ProcessedItem struct { RawName string Amount decimal.Decimal Price decimal.Decimal Sum decimal.Decimal IsMatched bool ProductID *uuid.UUID MatchSource string // "learned", "auto", "manual" } type ContainerForIndex struct { ID string `json:"id"` Name string `json:"name"` Count float64 `json:"count"` } type ProductForIndex struct { ID string `json:"id"` Name string `json:"name"` Code string `json:"code"` MeasureUnit string `json:"measure_unit"` Containers []ContainerForIndex `json:"containers"` } // GetCatalogForIndexing - возвращает облегченный каталог func (s *Service) GetCatalogForIndexing() ([]ProductForIndex, error) { products, err := s.catalogRepo.GetActiveGoods() if err != nil { return nil, err } result := make([]ProductForIndex, 0, len(products)) for _, p := range products { uom := "" if p.MainUnit != nil { uom = p.MainUnit.Name } var conts []ContainerForIndex for _, c := range p.Containers { cnt, _ := c.Count.Float64() conts = append(conts, ContainerForIndex{ ID: c.ID.String(), Name: c.Name, Count: cnt, }) } result = append(result, ProductForIndex{ ID: p.ID.String(), Name: p.Name, Code: p.Code, MeasureUnit: uom, Containers: conts, }) } return result, nil } // SaveMapping сохраняет привязку с количеством и фасовкой func (s *Service) SaveMapping(rawName string, productID uuid.UUID, quantity decimal.Decimal, containerID *uuid.UUID) error { return s.ocrRepo.SaveMatch(rawName, productID, quantity, containerID) } // GetKnownMatches возвращает список всех обученных связей func (s *Service) GetKnownMatches() ([]ocr.ProductMatch, error) { return s.ocrRepo.GetAllMatches() } // GetUnmatchedItems возвращает список частых нераспознанных строк func (s *Service) GetUnmatchedItems() ([]ocr.UnmatchedItem, error) { // Берем топ 50 нераспознанных return s.ocrRepo.GetTopUnmatched(50) } // FindKnownMatch ищет, знаем ли мы уже этот товар func (s *Service) FindKnownMatch(rawName string) (*ocr.ProductMatch, error) { return s.ocrRepo.FindMatch(rawName) } // SearchProducts ищет товары в БД по части названия (для ручного выбора в боте) func (s *Service) SearchProducts(query string) ([]catalog.Product, error) { // Этот метод нужно поддержать в репозитории, пока сделаем заглушку или фильтрацию в памяти // Для MVP добавим метод SearchByName в интерфейс репозитория all, err := s.catalogRepo.GetActiveGoods() if err != nil { return nil, err } // Простейший поиск в памяти (для начала хватит) query = strings.ToLower(query) var result []catalog.Product for _, p := range all { if strings.Contains(strings.ToLower(p.Name), query) { result = append(result, p) if len(result) >= 10 { // Ограничим выдачу break } } } return result, nil }