0202-финиш перед десктопом

пересчет поправил
редактирование с перепроведением
галка автопроведения работает
рекомендации починил
This commit is contained in:
2026-02-02 13:53:38 +03:00
parent 10882f55c8
commit 88620f3fb6
37 changed files with 1905 additions and 11162 deletions

View File

@@ -157,9 +157,6 @@ func (s *Service) UpdateDraftHeader(id uuid.UUID, storeID *uuid.UUID, supplierID
if err != nil {
return err
}
if draft.Status == drafts.StatusCompleted {
return errors.New("черновик уже отправлен")
}
draft.StoreID = storeID
draft.SupplierID = supplierID
draft.DateIncoming = &date
@@ -227,7 +224,7 @@ func (s *Service) DeleteItem(draftID, itemID uuid.UUID) (float64, error) {
return sumFloat, nil
}
// RecalculateItemFields - логика пересчета Qty/Price/Sum
// RecalculateItemFields - логика пересчета Q->P->S->Q (Quantity -> Price -> Sum -> Quantity) с использованием decimal для точности
func (s *Service) RecalculateItemFields(item *drafts.DraftInvoiceItem, editedField drafts.EditedField) {
if item.LastEditedField1 != editedField {
item.LastEditedField2 = item.LastEditedField1
@@ -265,6 +262,29 @@ func (s *Service) RecalculateItemFields(item *drafts.DraftInvoiceItem, editedFie
case drafts.FieldSum:
item.Sum = item.Quantity.Mul(item.Price)
}
// Дополнительная проверка для гарантии консистентности всех полей (Q->P->S->Q)
// Используется только для обеспечения точности, не влияет на логику выбора пересчитываемого поля
if !item.Price.IsZero() && !item.Quantity.IsZero() {
calculatedSum := item.Quantity.Mul(item.Price)
if !calculatedSum.Equal(item.Sum) {
item.Sum = calculatedSum
}
}
if !item.Price.IsZero() && !item.Sum.IsZero() {
calculatedQuantity := item.Sum.Div(item.Price)
if !calculatedQuantity.Equal(item.Quantity) {
item.Quantity = calculatedQuantity
}
}
if !item.Quantity.IsZero() && !item.Sum.IsZero() {
calculatedPrice := item.Sum.Div(item.Quantity)
if !calculatedPrice.Equal(item.Price) {
item.Price = calculatedPrice
}
}
}
// UpdateItem обновлен для поддержки динамического пересчета
@@ -293,17 +313,10 @@ func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, co
}
}
field := drafts.EditedField(editedField)
switch field {
case drafts.FieldQuantity:
currentItem.Quantity = qty
case drafts.FieldPrice:
currentItem.Price = price
case drafts.FieldSum:
currentItem.Sum = sum
}
s.RecalculateItemFields(currentItem, field)
// Просто присваиваем значения от фронтенда без пересчета
currentItem.Quantity = qty
currentItem.Price = price
currentItem.Sum = sum
if draft.Status == drafts.StatusCanceled {
draft.Status = drafts.StatusReadyToVerify
@@ -311,20 +324,18 @@ func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, co
}
updates := map[string]interface{}{
"product_id": currentItem.ProductID,
"container_id": currentItem.ContainerID,
"quantity": currentItem.Quantity,
"price": currentItem.Price,
"sum": currentItem.Sum,
"last_edited_field1": currentItem.LastEditedField1,
"last_edited_field2": currentItem.LastEditedField2,
"is_matched": currentItem.IsMatched,
"product_id": currentItem.ProductID,
"container_id": currentItem.ContainerID,
"quantity": currentItem.Quantity,
"price": currentItem.Price,
"sum": currentItem.Sum,
"is_matched": currentItem.IsMatched,
}
return s.draftRepo.UpdateItem(itemID, updates)
}
func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
func (s *Service) CommitDraft(draftID, userID uuid.UUID, isProcessed bool) (string, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil {
return "", fmt.Errorf("active server not found: %w", err)
@@ -347,17 +358,13 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
return "", errors.New("черновик принадлежит другому серверу")
}
if draft.Status == drafts.StatusCompleted {
return "", errors.New("накладная уже отправлена")
}
client, err := s.rmsFactory.GetClientForUser(userID)
if err != nil {
return "", err
}
targetStatus := "NEW"
if server.AutoProcess {
if isProcessed {
targetStatus = "PROCESSED"
}
@@ -373,6 +380,11 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
Items: make([]invoices.InvoiceItem, 0, len(draft.Items)),
}
// Если черновик уже был отправлен ранее, передаем RMSInvoiceID для обновления
if draft.RMSInvoiceID != nil {
inv.ID = *draft.RMSInvoiceID
}
for _, dItem := range draft.Items {
if dItem.ProductID == nil {
continue
@@ -405,6 +417,12 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
Price: priceToSend,
Sum: sum,
ContainerID: dItem.ContainerID,
Product: func() catalog.Product {
if dItem.Product != nil {
return *dItem.Product
}
return catalog.Product{}
}(),
}
inv.Items = append(inv.Items, invItem)
}
@@ -415,7 +433,22 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
docNum, err := client.CreateIncomingInvoice(inv)
if err != nil {
return "", err
// Если накладная уже проведена, пробуем распровести и повторить
if strings.Contains(err.Error(), "Changing processed") {
logger.Log.Info("Накладная проведена, выполняю распроведение...", zap.String("doc_num", draft.DocumentNumber))
if unprocessErr := client.UnprocessIncomingInvoice(inv); unprocessErr != nil {
return "", fmt.Errorf("не удалось распровести накладную: %w", unprocessErr)
}
// Повторяем попытку создания накладной после распроведения
docNum, err = client.CreateIncomingInvoice(inv)
if err != nil {
return "", err
}
} else {
return "", err
}
}
invoices, err := client.FetchInvoices(*draft.DateIncoming, *draft.DateIncoming)
@@ -434,6 +467,7 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
for _, invoice := range invoices {
if invoice.DocumentNumber == docNum {
draft.RMSInvoiceID = &invoice.ID
draft.DocumentNumber = invoice.DocumentNumber
found = true
break
}
@@ -555,19 +589,20 @@ func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID,
}
type UnifiedInvoiceDTO struct {
ID uuid.UUID `json:"id"`
Type string `json:"type"`
DocumentNumber string `json:"document_number"`
IncomingNumber string `json:"incoming_number"`
DateIncoming time.Time `json:"date_incoming"`
Status string `json:"status"`
TotalSum float64 `json:"total_sum"`
StoreName string `json:"store_name"`
ItemsCount int `json:"items_count"`
CreatedAt time.Time `json:"created_at"`
IsAppCreated bool `json:"is_app_created"`
PhotoURL string `json:"photo_url"`
ItemsPreview string `json:"items_preview"`
ID uuid.UUID `json:"id"`
Type string `json:"type"`
DocumentNumber string `json:"document_number"`
IncomingNumber string `json:"incoming_number"`
DateIncoming time.Time `json:"date_incoming"`
Status string `json:"status"`
TotalSum float64 `json:"total_sum"`
StoreName string `json:"store_name"`
ItemsCount int `json:"items_count"`
CreatedAt time.Time `json:"created_at"`
IsAppCreated bool `json:"is_app_created"`
PhotoURL string `json:"photo_url"`
ItemsPreview string `json:"items_preview"`
DraftID *uuid.UUID `json:"draft_id,omitempty"` // ID черновика для SYNCED накладных
}
func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]UnifiedInvoiceDTO, error) {
@@ -586,7 +621,7 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
return nil, err
}
photoMap, err := s.draftRepo.GetRMSInvoiceIDToPhotoURLMap(server.ID)
linkedDraftsMap, err := s.draftRepo.GetLinkedDraftsMap(server.ID)
if err != nil {
return nil, err
}
@@ -647,9 +682,11 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
isAppCreated := false
photoURL := ""
if url, exists := photoMap[inv.ID]; exists {
var draftID *uuid.UUID
if linkedInfo, exists := linkedDraftsMap[inv.ID]; exists {
isAppCreated = true
photoURL = url
photoURL = linkedInfo.PhotoURL
draftID = &linkedInfo.DraftID
}
var itemsPreview string
@@ -679,6 +716,7 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
IsAppCreated: isAppCreated,
PhotoURL: photoURL,
ItemsPreview: itemsPreview,
DraftID: draftID,
})
}
@@ -739,6 +777,14 @@ func (s *Service) CreateDraft(userID uuid.UUID) (*drafts.DraftInvoice, error) {
return nil, err
}
// Обновляем время последней активности сервера
if err := s.accountRepo.UpdateLastActivity(server.ID); err != nil {
logger.Log.Warn("Не удалось обновить время активности",
zap.String("server_id", server.ID.String()),
zap.Error(err))
// Не возвращаем ошибку - это некритично
}
return draft, nil
}
@@ -967,11 +1013,6 @@ func (s *Service) SaveDraftFull(draftID, userID uuid.UUID, req drafts.UpdateDraf
return err
}
// Проверяем, что черновик не завершен
if draft.Status == drafts.StatusCompleted {
return errors.New("черновик уже отправлен")
}
// Обновляем шапку черновика, если переданы поля
headerUpdated := false
@@ -1084,54 +1125,17 @@ func (s *Service) SaveDraftFull(draftID, userID uuid.UUID, req drafts.UpdateDraf
}
}
// Определяем, какое поле редактируется
editedField := itemReq.EditedField
if editedField == "" {
if itemReq.Sum != nil {
editedField = "sum"
} else if itemReq.Price != nil {
editedField = "price"
} else if itemReq.Quantity != nil {
editedField = "quantity"
}
}
// Обновляем числовые поля
qty := decimal.Zero
// Просто присваиваем значения от фронтенда без пересчета
if itemReq.Quantity != nil {
qty = decimal.NewFromFloat(*itemReq.Quantity)
} else {
qty = currentItem.Quantity
currentItem.Quantity = decimal.NewFromFloat(*itemReq.Quantity)
}
price := decimal.Zero
if itemReq.Price != nil {
price = decimal.NewFromFloat(*itemReq.Price)
} else {
price = currentItem.Price
currentItem.Price = decimal.NewFromFloat(*itemReq.Price)
}
sum := decimal.Zero
if itemReq.Sum != nil {
sum = decimal.NewFromFloat(*itemReq.Sum)
} else {
sum = currentItem.Sum
currentItem.Sum = decimal.NewFromFloat(*itemReq.Sum)
}
// Применяем изменения в зависимости от редактируемого поля
field := drafts.EditedField(editedField)
switch field {
case drafts.FieldQuantity:
currentItem.Quantity = qty
case drafts.FieldPrice:
currentItem.Price = price
case drafts.FieldSum:
currentItem.Sum = sum
}
// Пересчитываем поля
s.RecalculateItemFields(currentItem, field)
// Обновляем статус черновика, если он был отменен
if draft.Status == drafts.StatusCanceled {
draft.Status = drafts.StatusReadyToVerify
@@ -1142,14 +1146,12 @@ func (s *Service) SaveDraftFull(draftID, userID uuid.UUID, req drafts.UpdateDraf
// Сохраняем обновленную позицию
updates := map[string]interface{}{
"product_id": currentItem.ProductID,
"container_id": currentItem.ContainerID,
"quantity": currentItem.Quantity,
"price": currentItem.Price,
"sum": currentItem.Sum,
"last_edited_field1": currentItem.LastEditedField1,
"last_edited_field2": currentItem.LastEditedField2,
"is_matched": currentItem.IsMatched,
"product_id": currentItem.ProductID,
"container_id": currentItem.ContainerID,
"quantity": currentItem.Quantity,
"price": currentItem.Price,
"sum": currentItem.Sum,
"is_matched": currentItem.IsMatched,
}
if err := s.draftRepo.UpdateItem(itemID, updates); err != nil {
@@ -1158,5 +1160,13 @@ func (s *Service) SaveDraftFull(draftID, userID uuid.UUID, req drafts.UpdateDraf
}
}
// Обновляем время последней активности сервера
if err := s.accountRepo.UpdateLastActivity(draft.RMSServerID); err != nil {
logger.Log.Warn("Не удалось обновить время активности",
zap.String("server_id", draft.RMSServerID.String()),
zap.Error(err))
// Не возвращаем ошибку - это некритично
}
return nil
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/shopspring/decimal"
"go.uber.org/zap"
"rmser/internal/domain/account"
"rmser/internal/domain/drafts"
invDomain "rmser/internal/domain/invoices"
"rmser/internal/domain/suppliers"
@@ -19,16 +20,18 @@ type Service struct {
repo invDomain.Repository
draftsRepo drafts.Repository
supplierRepo suppliers.Repository
accountRepo account.Repository
rmsFactory *rms.Factory
// Здесь можно добавить репозитории каталога и контрагентов для валидации,
// но для краткости пока опустим глубокую валидацию.
}
func NewService(repo invDomain.Repository, draftsRepo drafts.Repository, supplierRepo suppliers.Repository, rmsFactory *rms.Factory) *Service {
func NewService(repo invDomain.Repository, draftsRepo drafts.Repository, supplierRepo suppliers.Repository, accountRepo account.Repository, rmsFactory *rms.Factory) *Service {
return &Service{
repo: repo,
draftsRepo: draftsRepo,
supplierRepo: supplierRepo,
accountRepo: accountRepo,
rmsFactory: rmsFactory,
}
}
@@ -99,6 +102,13 @@ func (s *Service) SendInvoiceToRMS(req CreateRequestDTO, userID uuid.UUID) (stri
return docNum, nil
}
// InvoiceStatsDTO - DTO для статистики накладных
type InvoiceStatsDTO struct {
Total int64 `json:"total"`
LastMonth int64 `json:"last_month"`
Last24h int64 `json:"last_24h"`
}
// InvoiceDetailsDTO - DTO для ответа на запрос деталей накладной
type InvoiceDetailsDTO struct {
ID uuid.UUID `json:"id"`
@@ -145,7 +155,7 @@ func (s *Service) GetInvoice(id uuid.UUID) (*InvoiceDetailsDTO, error) {
Number: inv.DocumentNumber,
Date: inv.DateIncoming.Format("2006-01-02"),
Status: "COMPLETED", // Для синхронизированных накладных статус всегда COMPLETED
Items: make([]struct {
Items: make([]struct {
Name string `json:"name"`
Quantity float64 `json:"quantity"`
Price float64 `json:"price"`
@@ -166,3 +176,32 @@ func (s *Service) GetInvoice(id uuid.UUID) (*InvoiceDetailsDTO, error) {
return dto, nil
}
// GetStats возвращает статистику по накладным для пользователя
func (s *Service) GetStats(userID uuid.UUID) (*InvoiceStatsDTO, error) {
// Получаем активный сервер пользователя
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil {
return nil, fmt.Errorf("ошибка получения активного сервера: %w", err)
}
if server == nil {
return &InvoiceStatsDTO{
Total: 0,
LastMonth: 0,
Last24h: 0,
}, nil
}
// Получаем статистику из репозитория
total, lastMonth, last24h, err := s.repo.GetStats(server.ID)
if err != nil {
return nil, fmt.Errorf("ошибка получения статистики: %w", err)
}
return &InvoiceStatsDTO{
Total: total,
LastMonth: lastMonth,
Last24h: last24h,
}, nil
}

View File

@@ -1,6 +1,7 @@
package recommend
import (
"github.com/google/uuid"
"go.uber.org/zap"
"rmser/internal/domain/recommendations"
@@ -20,56 +21,56 @@ func NewService(repo recommendations.Repository) *Service {
return &Service{repo: repo}
}
// RefreshRecommendations выполняет анализ и сохраняет результаты в БД
func (s *Service) RefreshRecommendations() error {
logger.Log.Info("Запуск пересчета рекомендаций...")
// RefreshRecommendations выполняет анализ и сохраняет результаты в БД для конкретного сервера
func (s *Service) RefreshRecommendations(serverID uuid.UUID) error {
logger.Log.Info("Запуск пересчета рекомендаций...", zap.String("server_id", serverID.String()))
var all []recommendations.Recommendation
// 1. Unused
if unused, err := s.repo.FindUnusedGoods(); err == nil {
if unused, err := s.repo.FindUnusedGoods(serverID); err == nil {
all = append(all, unused...)
} else {
logger.Log.Error("Ошибка unused", zap.Error(err))
}
// 2. Purchased but Unused
if purchUnused, err := s.repo.FindPurchasedButUnused(AnalyzeDaysNoIncoming); err == nil {
if purchUnused, err := s.repo.FindPurchasedButUnused(serverID, AnalyzeDaysNoIncoming); err == nil {
all = append(all, purchUnused...)
} else {
logger.Log.Error("Ошибка purchased_unused", zap.Error(err))
}
// 3. No Incoming (Ингредиенты без закупок)
if noInc, err := s.repo.FindNoIncomingIngredients(AnalyzeDaysNoIncoming); err == nil {
if noInc, err := s.repo.FindNoIncomingIngredients(serverID, AnalyzeDaysNoIncoming); err == nil {
all = append(all, noInc...)
} else {
logger.Log.Error("Ошибка no_incoming", zap.Error(err))
}
// 4. Usage without Purchase (Расход без прихода) <-- НОВОЕ
if usageNoPurch, err := s.repo.FindUsageWithoutPurchase(AnalyzeDaysNoIncoming); err == nil {
// 4. Usage without Purchase (Расход без прихода)
if usageNoPurch, err := s.repo.FindUsageWithoutPurchase(serverID, AnalyzeDaysNoIncoming); err == nil {
all = append(all, usageNoPurch...)
} else {
logger.Log.Error("Ошибка usage_no_purchase", zap.Error(err))
}
// 5. Stale (Неликвид)
if stale, err := s.repo.FindStaleGoods(AnalyzeDaysStale); err == nil {
if stale, err := s.repo.FindStaleGoods(serverID, AnalyzeDaysStale); err == nil {
all = append(all, stale...)
} else {
logger.Log.Error("Ошибка stale", zap.Error(err))
}
// 6. Dish in Recipe
if dishInRec, err := s.repo.FindDishesInRecipes(); err == nil {
if dishInRec, err := s.repo.FindDishesInRecipes(serverID); err == nil {
all = append(all, dishInRec...)
} else {
logger.Log.Error("Ошибка dish_in_recipe", zap.Error(err))
}
// Сохраняем
if err := s.repo.SaveAll(all); err != nil {
if err := s.repo.SaveAll(serverID, all); err != nil {
return err
}
@@ -77,6 +78,7 @@ func (s *Service) RefreshRecommendations() error {
return nil
}
func (s *Service) GetRecommendations() ([]recommendations.Recommendation, error) {
return s.repo.GetAll()
// GetRecommendations возвращает рекомендации для конкретного сервера
func (s *Service) GetRecommendations(serverID uuid.UUID) ([]recommendations.Recommendation, error) {
return s.repo.GetAll(serverID)
}

View File

@@ -15,6 +15,7 @@ import (
"rmser/internal/domain/recipes"
"rmser/internal/domain/suppliers"
"rmser/internal/infrastructure/rms"
"rmser/pkg/crypto"
"rmser/pkg/logger"
)
@@ -24,17 +25,19 @@ const (
)
type Service struct {
rmsFactory *rms.Factory
accountRepo account.Repository
catalogRepo catalog.Repository
recipeRepo recipes.Repository
invoiceRepo invoices.Repository
opRepo operations.Repository
supplierRepo suppliers.Repository
rmsFactory *rms.Factory
cryptoManager *crypto.CryptoManager
accountRepo account.Repository
catalogRepo catalog.Repository
recipeRepo recipes.Repository
invoiceRepo invoices.Repository
opRepo operations.Repository
supplierRepo suppliers.Repository
}
func NewService(
rmsFactory *rms.Factory,
cryptoManager *crypto.CryptoManager,
accountRepo account.Repository,
catalogRepo catalog.Repository,
recipeRepo recipes.Repository,
@@ -43,16 +46,73 @@ func NewService(
supplierRepo suppliers.Repository,
) *Service {
return &Service{
rmsFactory: rmsFactory,
accountRepo: accountRepo,
catalogRepo: catalogRepo,
recipeRepo: recipeRepo,
invoiceRepo: invoiceRepo,
opRepo: opRepo,
supplierRepo: supplierRepo,
rmsFactory: rmsFactory,
cryptoManager: cryptoManager,
accountRepo: accountRepo,
catalogRepo: catalogRepo,
recipeRepo: recipeRepo,
invoiceRepo: invoiceRepo,
opRepo: opRepo,
supplierRepo: supplierRepo,
}
}
// SyncAllDataForServer запускает полную синхронизацию для конкретного сервера
func (s *Service) SyncAllDataForServer(serverID uuid.UUID, force bool) error {
logger.Log.Info("Запуск синхронизации по серверу", zap.String("server_id", serverID.String()), zap.Bool("force", force))
// 1. Получаем информацию о сервере
server, err := s.accountRepo.GetServerByID(serverID)
if err != nil || server == nil {
return fmt.Errorf("server not found: %s", serverID)
}
// 2. Получаем креды владельца сервера для подключения
baseURL, login, encryptedPass, err := s.getOwnerCredentials(serverID)
if err != nil {
return fmt.Errorf("failed to get owner credentials: %w", err)
}
// 3. Расшифровываем пароль
plainPass, err := s.cryptoManager.Decrypt(encryptedPass)
if err != nil {
return fmt.Errorf("failed to decrypt password: %w", err)
}
// 4. Создаем клиент RMS
client := s.rmsFactory.CreateClientFromRawCredentials(baseURL, login, plainPass)
return s.syncAllWithClient(client, serverID, force)
}
// getOwnerCredentials возвращает учетные данные владельца сервера
func (s *Service) getOwnerCredentials(serverID uuid.UUID) (url, login, encryptedPass string, err error) {
// Находим владельца сервера
users, err := s.accountRepo.GetServerUsers(serverID)
if err != nil {
return "", "", "", err
}
var ownerLink *account.ServerUser
for i := range users {
if users[i].Role == account.RoleOwner {
ownerLink = &users[i]
break
}
}
if ownerLink == nil {
return "", "", "", fmt.Errorf("owner not found for server %s", serverID)
}
// Если у владельца есть личные креды - используем их
if ownerLink.Login != "" && ownerLink.EncryptedPassword != "" {
return ownerLink.Server.BaseURL, ownerLink.Login, ownerLink.EncryptedPassword, nil
}
return "", "", "", fmt.Errorf("owner has no credentials for server %s", serverID)
}
// SyncAllData запускает полную синхронизацию для конкретного пользователя
func (s *Service) SyncAllData(userID uuid.UUID, force bool) error {
logger.Log.Info("Запуск синхронизации", zap.String("user_id", userID.String()), zap.Bool("force", force))
@@ -68,6 +128,12 @@ func (s *Service) SyncAllData(userID uuid.UUID, force bool) error {
}
serverID := server.ID
return s.syncAllWithClient(client, serverID, force)
}
// syncAllWithClient выполняет синхронизацию с готовым клиентом
func (s *Service) syncAllWithClient(client rms.ClientI, serverID uuid.UUID, force bool) error {
// 2. Справочники
if err := s.syncStores(client, serverID); err != nil {
logger.Log.Error("Sync Stores failed", zap.Error(err))
@@ -96,13 +162,12 @@ func (s *Service) SyncAllData(userID uuid.UUID, force bool) error {
logger.Log.Error("Sync Invoices failed", zap.Error(err))
}
// 7. Складские операции (тяжелый запрос)
// Для MVP можно отключить, если долго грузится
// if err := s.SyncStoreOperations(client, serverID); err != nil {
// logger.Log.Error("Sync Operations failed", zap.Error(err))
// }
// 7. Складские операции
if err := s.SyncStoreOperations(client, serverID); err != nil {
logger.Log.Error("Sync Operations failed", zap.Error(err))
}
logger.Log.Info("Синхронизация завершена", zap.String("user_id", userID.String()))
logger.Log.Info("Синхронизация завершена", zap.String("server_id", serverID.String()))
return nil
}
@@ -236,7 +301,7 @@ func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID, force bool) er
// SyncStoreOperations публичный, если нужно вызывать отдельно
func (s *Service) SyncStoreOperations(c rms.ClientI, serverID uuid.UUID) error {
dateTo := time.Now()
dateFrom := dateTo.AddDate(0, 0, -30)
dateFrom := dateTo.AddDate(0, 0, -90) // 90 дней — соответствует периоду анализа рекомендаций
if err := s.syncReport(c, serverID, PresetPurchases, operations.OpTypePurchase, dateFrom, dateTo); err != nil {
return fmt.Errorf("purchases sync error: %w", err)

View File

@@ -0,0 +1,136 @@
package worker
import (
"context"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"rmser/internal/domain/account"
"rmser/internal/infrastructure/rms"
"rmser/internal/services/recommend"
)
// SyncService интерфейс для синхронизации данных
type SyncService interface {
// SyncAllDataForServer синхронизирует данные для конкретного сервера
SyncAllDataForServer(serverID uuid.UUID, force bool) error
}
// SyncWorker фоновый процесс для автоматической синхронизации данных с iiko серверами
type SyncWorker struct {
syncService SyncService // сервис для синхронизации
accountRepo account.Repository // репозиторий для работы с серверами
rmsFactory *rms.Factory // фабрика для создания клиентов RMS
recService *recommend.Service // сервис рекомендаций
logger *zap.Logger
tickerInterval time.Duration // интервал проверки (например, 1 минута)
idleThreshold time.Duration // порог простоя (10 минут)
}
// NewSyncWorker создает новый экземпляр SyncWorker
func NewSyncWorker(
syncService SyncService,
accountRepo account.Repository,
rmsFactory *rms.Factory,
recService *recommend.Service,
logger *zap.Logger,
) *SyncWorker {
return &SyncWorker{
syncService: syncService,
accountRepo: accountRepo,
rmsFactory: rmsFactory,
recService: recService,
logger: logger,
tickerInterval: 1 * time.Minute,
idleThreshold: 10 * time.Minute,
}
}
// Run запускает фоновый процесс синхронизации
func (w *SyncWorker) Run(ctx context.Context) {
w.logger.Info("Запуск SyncWorker",
zap.Duration("ticker_interval", w.tickerInterval),
zap.Duration("idle_threshold", w.idleThreshold))
ticker := time.NewTicker(w.tickerInterval)
defer ticker.Stop()
// Первый запуск сразу
w.processSync(ctx)
for {
select {
case <-ctx.Done():
w.logger.Info("Остановка SyncWorker")
return
case <-ticker.C:
w.processSync(ctx)
}
}
}
// processSync обрабатывает синхронизацию для всех серверов, готовых к синхронизации
func (w *SyncWorker) processSync(ctx context.Context) {
// Получаем серверы, готовые для синхронизации
servers, err := w.accountRepo.GetServersForSync(w.idleThreshold)
if err != nil {
w.logger.Error("Ошибка получения серверов для синхронизации", zap.Error(err))
return
}
if len(servers) == 0 {
return
}
w.logger.Info("Найдены серверы для синхронизации",
zap.Int("count", len(servers)))
for _, server := range servers {
// Обрабатываем каждый сервер в отдельной горутине
go w.syncServer(ctx, server)
}
}
// syncServer выполняет синхронизацию для конкретного сервера
func (w *SyncWorker) syncServer(ctx context.Context, server account.RMSServer) {
defer func() {
if r := recover(); r != nil {
w.logger.Error("Паника при синхронизации сервера",
zap.String("server_id", server.ID.String()),
zap.Any("recover", r))
}
}()
w.logger.Info("Начало синхронизации сервера",
zap.String("server_id", server.ID.String()),
zap.String("server_name", server.Name))
// Вызываем синхронизацию через syncService
err := w.syncService.SyncAllDataForServer(server.ID, false)
if err != nil {
w.logger.Error("Ошибка синхронизации сервера",
zap.String("server_id", server.ID.String()),
zap.Error(err))
return
}
// Обновляем время последней синхронизации
err = w.accountRepo.UpdateLastSync(server.ID)
if err != nil {
w.logger.Error("Ошибка обновления времени синхронизации",
zap.String("server_id", server.ID.String()),
zap.Error(err))
}
// Обновляем рекомендации после успешной синхронизации
if err := w.recService.RefreshRecommendations(server.ID); err != nil {
w.logger.Error("Ошибка обновления рекомендаций",
zap.String("server_id", server.ID.String()),
zap.Error(err))
}
w.logger.Info("Синхронизация сервера завершена успешно",
zap.String("server_id", server.ID.String()))
}