Files
rmser/internal/services/ocr/service.go
2025-12-11 05:20:53 +03:00

178 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}