Files
rmser/internal/services/ocr/service.go
SERTY b4ce819931 добавил пользователей для сервера и роли
добавил инвайт-ссылки с ролью оператор для сервера
добавил супер-админку для смены владельцев
добавил уведомления о смене ролей на серверах
добавил модалку для фото прям в черновике
добавил UI для редактирования прав
2025-12-23 13:06:06 +03:00

252 lines
7.0 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"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"rmser/internal/domain/account"
"rmser/internal/domain/catalog"
"rmser/internal/domain/drafts"
"rmser/internal/domain/ocr"
"rmser/internal/infrastructure/ocr_client"
)
type Service struct {
ocrRepo ocr.Repository
catalogRepo catalog.Repository
draftRepo drafts.Repository
accountRepo account.Repository
pyClient *ocr_client.Client
storagePath string
}
func NewService(
ocrRepo ocr.Repository,
catalogRepo catalog.Repository,
draftRepo drafts.Repository,
accountRepo account.Repository,
pyClient *ocr_client.Client,
storagePath string,
) *Service {
return &Service{
ocrRepo: ocrRepo,
catalogRepo: catalogRepo,
draftRepo: draftRepo,
accountRepo: accountRepo,
pyClient: pyClient,
storagePath: storagePath,
}
}
// checkWriteAccess - вспомогательный метод проверки прав
func (s *Service) checkWriteAccess(userID, serverID uuid.UUID) error {
role, err := s.accountRepo.GetUserRole(userID, serverID)
if err != nil {
return err
}
if role == account.RoleOperator {
return errors.New("access denied: operators cannot modify data")
}
return nil
}
// ProcessReceiptImage - Доступно всем (включая Операторов)
func (s *Service) ProcessReceiptImage(ctx context.Context, userID uuid.UUID, imgData []byte) (*drafts.DraftInvoice, error) {
// 1. Получаем активный сервер для UserID
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("no active server for user")
}
serverID := server.ID
// 2. Создаем черновик
draft := &drafts.DraftInvoice{
UserID: userID,
RMSServerID: serverID,
Status: drafts.StatusProcessing,
StoreID: server.DefaultStoreID,
}
draft.ID = uuid.New()
fileName := fmt.Sprintf("receipt_%s.jpg", draft.ID.String())
filePath := filepath.Join(s.storagePath, fileName)
if err := os.WriteFile(filePath, imgData, 0644); err != nil {
return nil, fmt.Errorf("failed to save image: %w", err)
}
draft.SenderPhotoURL = "/uploads/" + fileName
if err := s.draftRepo.Create(draft); err != nil {
return nil, fmt.Errorf("failed to create draft: %w", err)
}
// 3. Отправляем в Python OCR
rawResult, err := s.pyClient.ProcessImage(ctx, imgData, "receipt.jpg")
if err != nil {
draft.Status = drafts.StatusError
_ = s.draftRepo.Update(draft)
return nil, fmt.Errorf("python ocr error: %w", err)
}
// 4. Матчинг (с учетом ServerID)
var draftItems []drafts.DraftInvoiceItem
for _, rawItem := range rawResult.Items {
item := drafts.DraftInvoiceItem{
DraftID: draft.ID,
RawName: rawItem.RawName,
RawAmount: decimal.NewFromFloat(rawItem.Amount),
RawPrice: decimal.NewFromFloat(rawItem.Price),
Quantity: decimal.NewFromFloat(rawItem.Amount),
Price: decimal.NewFromFloat(rawItem.Price),
Sum: decimal.NewFromFloat(rawItem.Sum),
}
match, _ := s.ocrRepo.FindMatch(serverID, rawItem.RawName)
if match != nil {
item.IsMatched = true
item.ProductID = &match.ProductID
item.ContainerID = match.ContainerID
} else {
s.ocrRepo.UpsertUnmatched(serverID, rawItem.RawName) // <-- ServerID
}
draftItems = append(draftItems, item)
}
// 5. Сохраняем
draft.Status = drafts.StatusReadyToVerify
s.draftRepo.Update(draft)
s.draftRepo.CreateItems(draftItems)
draft.Items = draftItems
return draft, nil
}
// Добавить структуры в конец файла
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(userID uuid.UUID) ([]ProductForIndex, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("no server")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
products, err := s.catalogRepo.GetActiveGoods(server.ID, server.RootGroupGUID)
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
}
func (s *Service) SearchProducts(userID uuid.UUID, query string) ([]catalog.Product, error) {
if len(query) < 2 {
return []catalog.Product{}, nil
}
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("no server")
}
// Поиск нужен для матчинга, значит тоже защищаем
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
return s.catalogRepo.Search(server.ID, query, server.RootGroupGUID)
}
func (s *Service) SaveMapping(userID uuid.UUID, rawName string, productID uuid.UUID, quantity decimal.Decimal, containerID *uuid.UUID) error {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return fmt.Errorf("no server")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return err
}
return s.ocrRepo.SaveMatch(server.ID, rawName, productID, quantity, containerID)
}
func (s *Service) DeleteMatch(userID uuid.UUID, rawName string) error {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return fmt.Errorf("no server")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return err
}
return s.ocrRepo.DeleteMatch(server.ID, rawName)
}
func (s *Service) GetKnownMatches(userID uuid.UUID) ([]ocr.ProductMatch, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("no server")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
return s.ocrRepo.GetAllMatches(server.ID)
}
func (s *Service) GetUnmatchedItems(userID uuid.UUID) ([]ocr.UnmatchedItem, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("no server")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
return s.ocrRepo.GetTopUnmatched(server.ID, 50)
}