start rmser

This commit is contained in:
2025-11-29 08:40:24 +03:00
commit 5aa2238eea
2117 changed files with 375169 additions and 0 deletions

View 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
}