mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
start rmser
This commit is contained in:
147
internal/services/ocr/service.go
Normal file
147
internal/services/ocr/service.go
Normal file
@@ -0,0 +1,147 @@
|
||||
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
|
||||
|
||||
// 2. Обрабатываем каждую строку
|
||||
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),
|
||||
}
|
||||
|
||||
// 3. Ищем соответствие
|
||||
// Сначала проверяем таблицу ручного обучения (product_matches)
|
||||
matchID, err := s.ocrRepo.FindMatch(rawItem.RawName)
|
||||
if err != nil {
|
||||
logger.Log.Error("db error finding match", zap.Error(err))
|
||||
}
|
||||
|
||||
if matchID != nil {
|
||||
// Нашли в обучении
|
||||
item.ProductID = matchID
|
||||
item.IsMatched = true
|
||||
item.MatchSource = "learned"
|
||||
} else {
|
||||
// Если не нашли, пробуем найти точное совпадение по имени в каталоге (на всякий случай)
|
||||
// (В реальном проекте тут может быть нечеткий поиск, но пока точный)
|
||||
// TODO: Добавить метод FindByName в репозиторий каталога, если нужно
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
// ProductForIndex DTO для внешнего сервиса
|
||||
type ProductForIndex struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// 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 {
|
||||
result = append(result, ProductForIndex{
|
||||
ID: p.ID.String(),
|
||||
Name: p.Name,
|
||||
Code: p.Code,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SaveMapping сохраняет связь "Текст из чека" -> "Наш товар"
|
||||
func (s *Service) SaveMapping(rawName string, productID uuid.UUID) error {
|
||||
return s.ocrRepo.SaveMatch(rawName, productID)
|
||||
}
|
||||
|
||||
// FindKnownMatch ищет, знаем ли мы уже этот товар
|
||||
func (s *Service) FindKnownMatch(rawName string) (*uuid.UUID, 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
|
||||
}
|
||||
Reference in New Issue
Block a user