mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
0202-финиш перед десктопом
пересчет поправил редактирование с перепроведением галка автопроведения работает рекомендации починил
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
136
internal/services/worker/sync_worker.go
Normal file
136
internal/services/worker/sync_worker.go
Normal 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()))
|
||||
}
|
||||
Reference in New Issue
Block a user