mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
Перевел на multi-tenant
Добавил поставщиков Накладные успешно создаются из фронта
This commit is contained in:
117
internal/infrastructure/repository/account/postgres.go
Normal file
117
internal/infrastructure/repository/account/postgres.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"rmser/internal/domain/account"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type pgRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) account.Repository {
|
||||
return &pgRepository{db: db}
|
||||
}
|
||||
|
||||
// GetOrCreateUser находит пользователя или создает нового
|
||||
func (r *pgRepository) GetOrCreateUser(telegramID int64, username, first, last string) (*account.User, error) {
|
||||
var user account.User
|
||||
err := r.db.Where("telegram_id = ?", telegramID).First(&user).Error
|
||||
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// Создаем
|
||||
newUser := account.User{
|
||||
TelegramID: telegramID,
|
||||
Username: username,
|
||||
FirstName: first,
|
||||
LastName: last,
|
||||
}
|
||||
if err := r.db.Create(&newUser).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &newUser, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Обновляем инфо, если изменилось (опционально)
|
||||
if user.Username != username || user.FirstName != first {
|
||||
user.Username = username
|
||||
user.FirstName = first
|
||||
user.LastName = last
|
||||
r.db.Save(&user)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *pgRepository) GetUserByTelegramID(telegramID int64) (*account.User, error) {
|
||||
var user account.User
|
||||
// Preload Servers чтобы сразу видеть подключения
|
||||
err := r.db.Preload("Servers").Where("telegram_id = ?", telegramID).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *pgRepository) SaveServer(server *account.RMSServer) error {
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
UpdateAll: true,
|
||||
}).Create(server).Error
|
||||
}
|
||||
|
||||
// SetActiveServer делает указанный сервер активным, а остальные — неактивными
|
||||
func (r *pgRepository) SetActiveServer(userID, serverID uuid.UUID) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. Сбрасываем флаг у всех серверов пользователя
|
||||
if err := tx.Model(&account.RMSServer{}).
|
||||
Where("user_id = ?", userID).
|
||||
Update("is_active", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Ставим флаг целевому серверу
|
||||
if err := tx.Model(&account.RMSServer{}).
|
||||
Where("id = ? AND user_id = ?", serverID, userID).
|
||||
Update("is_active", true).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *pgRepository) GetActiveServer(userID uuid.UUID) (*account.RMSServer, error) {
|
||||
var server account.RMSServer
|
||||
// Берем первый активный сервер. В будущем можно добавить поле IsSelected
|
||||
err := r.db.Where("user_id = ? AND is_active = ?", userID, true).First(&server).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil // Нет серверов
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &server, nil
|
||||
}
|
||||
|
||||
func (r *pgRepository) GetAllServers(userID uuid.UUID) ([]account.RMSServer, error) {
|
||||
var servers []account.RMSServer
|
||||
err := r.db.Where("user_id = ? AND is_active = ?", userID, true).Find(&servers).Error
|
||||
return servers, err
|
||||
}
|
||||
|
||||
func (r *pgRepository) DeleteServer(serverID uuid.UUID) error {
|
||||
return r.db.Delete(&account.RMSServer{}, serverID).Error
|
||||
}
|
||||
|
||||
func (r *pgRepository) IncrementInvoiceCount(serverID uuid.UUID) error {
|
||||
return r.db.Model(&account.RMSServer{}).
|
||||
Where("id = ?", serverID).
|
||||
UpdateColumn("invoice_count", gorm.Expr("invoice_count + ?", 1)).Error
|
||||
}
|
||||
@@ -16,12 +16,17 @@ func NewRepository(db *gorm.DB) catalog.Repository {
|
||||
return &pgRepository{db: db}
|
||||
}
|
||||
|
||||
// --- Запись (Save) ---
|
||||
// При сохранении мы предполагаем, что serverID уже проставлен в Entity в слое Service.
|
||||
// Но для надежности можно передавать serverID в метод Save, однако Service должен это контролировать.
|
||||
// Оставим контракт Save(products []Product), где внутри products уже заполнен RMSServerID.
|
||||
|
||||
func (r *pgRepository) SaveMeasureUnits(units []catalog.MeasureUnit) error {
|
||||
if len(units) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
Columns: []clause.Column{{Name: "id"}}, // ID глобально уникален (UUID), конфликтов между серверами не будет
|
||||
UpdateAll: true,
|
||||
}).CreateInBatches(units, 100).Error
|
||||
}
|
||||
@@ -29,7 +34,7 @@ func (r *pgRepository) SaveMeasureUnits(units []catalog.MeasureUnit) error {
|
||||
func (r *pgRepository) SaveProducts(products []catalog.Product) error {
|
||||
sorted := sortProductsByHierarchy(products)
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. Сохраняем продукты (без контейнеров, чтобы ускорить и не дублировать)
|
||||
// 1. Продукты
|
||||
if err := tx.Omit("Containers").Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
UpdateAll: true,
|
||||
@@ -37,13 +42,12 @@ func (r *pgRepository) SaveProducts(products []catalog.Product) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Собираем все контейнеры в один слайс
|
||||
// 2. Контейнеры
|
||||
var allContainers []catalog.ProductContainer
|
||||
for _, p := range products {
|
||||
allContainers = append(allContainers, p.Containers...)
|
||||
}
|
||||
|
||||
// 3. Сохраняем контейнеры
|
||||
if len(allContainers) > 0 {
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
@@ -56,13 +60,66 @@ func (r *pgRepository) SaveProducts(products []catalog.Product) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (r *pgRepository) SaveContainer(container catalog.ProductContainer) error {
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
UpdateAll: true,
|
||||
}).Create(&container).Error
|
||||
}
|
||||
|
||||
func (r *pgRepository) SaveStores(stores []catalog.Store) error {
|
||||
if len(stores) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
UpdateAll: true,
|
||||
}).CreateInBatches(stores, 100).Error
|
||||
}
|
||||
|
||||
// --- Чтение (Read) с фильтрацией по ServerID ---
|
||||
|
||||
func (r *pgRepository) GetAll() ([]catalog.Product, error) {
|
||||
// Этот метод был legacy и грузил всё. Теперь он опасен без serverID.
|
||||
// Оставляем заглушку или удаляем. Лучше удалить из интерфейса, но пока вернем пустой список
|
||||
// чтобы не ломать сборку, пока не почистим вызовы.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *pgRepository) GetActiveGoods(serverID uuid.UUID) ([]catalog.Product, error) {
|
||||
var products []catalog.Product
|
||||
err := r.db.Preload("MainUnit").Find(&products).Error
|
||||
err := r.db.
|
||||
Preload("MainUnit").
|
||||
Preload("Containers").
|
||||
Where("rms_server_id = ? AND is_deleted = ? AND type IN ?", serverID, false, []string{"GOODS"}).
|
||||
Order("name ASC").
|
||||
Find(&products).Error
|
||||
return products, err
|
||||
}
|
||||
|
||||
// Вспомогательная функция сортировки (оставляем как была)
|
||||
func (r *pgRepository) GetActiveStores(serverID uuid.UUID) ([]catalog.Store, error) {
|
||||
var stores []catalog.Store
|
||||
err := r.db.Where("rms_server_id = ? AND is_deleted = ?", serverID, false).Order("name ASC").Find(&stores).Error
|
||||
return stores, err
|
||||
}
|
||||
|
||||
func (r *pgRepository) Search(serverID uuid.UUID, query string) ([]catalog.Product, error) {
|
||||
var products []catalog.Product
|
||||
q := "%" + query + "%"
|
||||
|
||||
err := r.db.
|
||||
Preload("MainUnit").
|
||||
Preload("Containers").
|
||||
Where("rms_server_id = ? AND is_deleted = ? AND type = ?", serverID, false, "GOODS").
|
||||
Where("name ILIKE ? OR code ILIKE ? OR num ILIKE ?", q, q, q).
|
||||
Order("name ASC").
|
||||
Limit(20).
|
||||
Find(&products).Error
|
||||
|
||||
return products, err
|
||||
}
|
||||
|
||||
// sortProductsByHierarchy - вспомогательная функция, оставляем как есть (копипаст из старого файла)
|
||||
func sortProductsByHierarchy(products []catalog.Product) []catalog.Product {
|
||||
if len(products) == 0 {
|
||||
return products
|
||||
@@ -104,58 +161,18 @@ func sortProductsByHierarchy(products []catalog.Product) []catalog.Product {
|
||||
return result
|
||||
}
|
||||
|
||||
// GetActiveGoods возвращает только активные товары c подгруженной единицей измерения
|
||||
// GetActiveGoods оптимизирован: подгружаем Units и Containers
|
||||
func (r *pgRepository) GetActiveGoods() ([]catalog.Product, error) {
|
||||
var products []catalog.Product
|
||||
err := r.db.
|
||||
Preload("MainUnit").
|
||||
Preload("Containers"). // <-- Подгружаем фасовки
|
||||
Where("is_deleted = ? AND type IN ?", false, []string{"GOODS"}).
|
||||
Order("name ASC").
|
||||
Find(&products).Error
|
||||
return products, err
|
||||
func (r *pgRepository) CountGoods(serverID uuid.UUID) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&catalog.Product{}).
|
||||
Where("rms_server_id = ? AND type IN ? AND is_deleted = ?", serverID, []string{"GOODS"}, false).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *pgRepository) SaveStores(stores []catalog.Store) error {
|
||||
if len(stores) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
UpdateAll: true,
|
||||
}).CreateInBatches(stores, 100).Error
|
||||
}
|
||||
|
||||
func (r *pgRepository) GetActiveStores() ([]catalog.Store, error) {
|
||||
var stores []catalog.Store
|
||||
err := r.db.Where("is_deleted = ?", false).Order("name ASC").Find(&stores).Error
|
||||
return stores, err
|
||||
}
|
||||
|
||||
// SaveContainer сохраняет или обновляет одну фасовку
|
||||
func (r *pgRepository) SaveContainer(container catalog.ProductContainer) error {
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
UpdateAll: true,
|
||||
}).Create(&container).Error
|
||||
}
|
||||
|
||||
// Search ищет товары по названию, артикулу или коду (ILIKE)
|
||||
func (r *pgRepository) Search(query string) ([]catalog.Product, error) {
|
||||
var products []catalog.Product
|
||||
|
||||
// Оборачиваем в проценты для поиска подстроки
|
||||
q := "%" + query + "%"
|
||||
|
||||
err := r.db.
|
||||
Preload("MainUnit").
|
||||
Preload("Containers"). // Обязательно грузим фасовки, они нужны для выбора
|
||||
Where("is_deleted = ? AND type = ?", false, "GOODS").
|
||||
Where("name ILIKE ? OR code ILIKE ? OR num ILIKE ?", q, q, q).
|
||||
Order("name ASC").
|
||||
Limit(20). // Ограничиваем выдачу, чтобы не перегружать фронт
|
||||
Find(&products).Error
|
||||
|
||||
return products, err
|
||||
func (r *pgRepository) CountStores(serverID uuid.UUID) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&catalog.Store{}).
|
||||
Where("rms_server_id = ? AND is_deleted = ?", serverID, false).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func (r *pgRepository) GetByID(id uuid.UUID) (*drafts.DraftInvoice, error) {
|
||||
return db.Order("draft_invoice_items.raw_name ASC")
|
||||
}).
|
||||
Preload("Items.Product").
|
||||
Preload("Items.Product.MainUnit"). // Нужно для отображения единиц
|
||||
Preload("Items.Product.MainUnit").
|
||||
Preload("Items.Container").
|
||||
Where("id = ?", id).
|
||||
First(&draft).Error
|
||||
@@ -39,7 +39,7 @@ func (r *pgRepository) GetByID(id uuid.UUID) (*drafts.DraftInvoice, error) {
|
||||
}
|
||||
|
||||
func (r *pgRepository) Update(draft *drafts.DraftInvoice) error {
|
||||
// Обновляем только основные поля шапки
|
||||
// Обновляем поля шапки + привязки к серверу
|
||||
return r.db.Model(draft).Updates(map[string]interface{}{
|
||||
"status": draft.Status,
|
||||
"document_number": draft.DocumentNumber,
|
||||
@@ -48,6 +48,7 @@ func (r *pgRepository) Update(draft *drafts.DraftInvoice) error {
|
||||
"store_id": draft.StoreID,
|
||||
"comment": draft.Comment,
|
||||
"rms_invoice_id": draft.RMSInvoiceID,
|
||||
"rms_server_id": draft.RMSServerID, // Вдруг поменялся, хотя не должен
|
||||
"updated_at": gorm.Expr("NOW()"),
|
||||
}).Error
|
||||
}
|
||||
@@ -60,34 +61,29 @@ func (r *pgRepository) CreateItems(items []drafts.DraftInvoiceItem) error {
|
||||
}
|
||||
|
||||
func (r *pgRepository) UpdateItem(itemID uuid.UUID, productID *uuid.UUID, containerID *uuid.UUID, qty, price decimal.Decimal) error {
|
||||
// Пересчитываем сумму
|
||||
sum := qty.Mul(price)
|
||||
|
||||
// Определяем статус IsMatched: если productID задан - значит сматчено
|
||||
isMatched := productID != nil
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"product_id": productID,
|
||||
"container_id": containerID,
|
||||
"quantity": qty,
|
||||
"price": price,
|
||||
"sum": sum,
|
||||
"is_matched": isMatched,
|
||||
}
|
||||
|
||||
return r.db.Model(&drafts.DraftInvoiceItem{}).
|
||||
Where("id = ?", itemID).
|
||||
Updates(updates).Error
|
||||
Updates(map[string]interface{}{
|
||||
"product_id": productID,
|
||||
"container_id": containerID,
|
||||
"quantity": qty,
|
||||
"price": price,
|
||||
"sum": sum,
|
||||
"is_matched": isMatched,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *pgRepository) Delete(id uuid.UUID) error {
|
||||
return r.db.Delete(&drafts.DraftInvoice{}, id).Error
|
||||
}
|
||||
|
||||
func (r *pgRepository) GetActive() ([]drafts.DraftInvoice, error) {
|
||||
// GetActive фильтрует по UserID
|
||||
func (r *pgRepository) GetActive(userID uuid.UUID) ([]drafts.DraftInvoice, error) {
|
||||
var list []drafts.DraftInvoice
|
||||
|
||||
// Выбираем статусы, которые считаем "активными"
|
||||
activeStatuses := []string{
|
||||
drafts.StatusProcessing,
|
||||
drafts.StatusReadyToVerify,
|
||||
@@ -96,9 +92,9 @@ func (r *pgRepository) GetActive() ([]drafts.DraftInvoice, error) {
|
||||
}
|
||||
|
||||
err := r.db.
|
||||
Preload("Items"). // Нужны для подсчета суммы и количества
|
||||
Preload("Store"). // Нужно для названия склада
|
||||
Where("status IN ?", activeStatuses).
|
||||
Preload("Items").
|
||||
Preload("Store").
|
||||
Where("user_id = ? AND status IN ?", userID, activeStatuses). // <-- FILTER
|
||||
Order("created_at DESC").
|
||||
Find(&list).Error
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"rmser/internal/domain/invoices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
@@ -17,9 +18,10 @@ func NewRepository(db *gorm.DB) invoices.Repository {
|
||||
return &pgRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *pgRepository) GetLastInvoiceDate() (*time.Time, error) {
|
||||
func (r *pgRepository) GetLastInvoiceDate(serverID uuid.UUID) (*time.Time, error) {
|
||||
var inv invoices.Invoice
|
||||
err := r.db.Order("date_incoming DESC").First(&inv).Error
|
||||
// Ищем последнюю накладную только для этого сервера
|
||||
err := r.db.Where("rms_server_id = ?", serverID).Order("date_incoming DESC").First(&inv).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
@@ -38,6 +40,7 @@ func (r *pgRepository) SaveInvoices(list []invoices.Invoice) error {
|
||||
}).Create(&inv).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Удаляем старые Items для этой накладной
|
||||
if err := tx.Where("invoice_id = ?", inv.ID).Delete(&invoices.InvoiceItem{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -50,3 +53,13 @@ func (r *pgRepository) SaveInvoices(list []invoices.Invoice) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *pgRepository) CountRecent(serverID uuid.UUID, days int) (int64, error) {
|
||||
var count int64
|
||||
dateFrom := time.Now().AddDate(0, 0, -days)
|
||||
|
||||
err := r.db.Model(&invoices.Invoice{}).
|
||||
Where("rms_server_id = ? AND date_incoming >= ?", serverID, dateFrom).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
@@ -21,9 +21,11 @@ func NewRepository(db *gorm.DB) ocr.Repository {
|
||||
return &pgRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *pgRepository) SaveMatch(rawName string, productID uuid.UUID, quantity decimal.Decimal, containerID *uuid.UUID) error {
|
||||
func (r *pgRepository) SaveMatch(serverID uuid.UUID, rawName string, productID uuid.UUID, quantity decimal.Decimal, containerID *uuid.UUID) error {
|
||||
normalized := strings.ToLower(strings.TrimSpace(rawName))
|
||||
|
||||
match := ocr.ProductMatch{
|
||||
RMSServerID: serverID,
|
||||
RawName: normalized,
|
||||
ProductID: productID,
|
||||
Quantity: quantity,
|
||||
@@ -31,31 +33,40 @@ func (r *pgRepository) SaveMatch(rawName string, productID uuid.UUID, quantity d
|
||||
}
|
||||
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// Используем OnConflict по составному индексу (raw_name, rms_server_id)
|
||||
// Но GORM может потребовать названия ограничения.
|
||||
// Проще сделать через Where().Assign().FirstOrCreate() или явно указать Columns если индекс есть.
|
||||
// В Entity мы указали `uniqueIndex:idx_raw_server`.
|
||||
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "raw_name"}},
|
||||
// Указываем оба поля, входящие в unique index
|
||||
Columns: []clause.Column{{Name: "raw_name"}, {Name: "rms_server_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"product_id", "quantity", "container_id", "updated_at"}),
|
||||
}).Create(&match).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Where("raw_name = ?", normalized).Delete(&ocr.UnmatchedItem{}).Error; err != nil {
|
||||
// Удаляем из Unmatched для этого сервера
|
||||
if err := tx.Where("rms_server_id = ? AND raw_name = ?", serverID, normalized).Delete(&ocr.UnmatchedItem{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *pgRepository) DeleteMatch(rawName string) error {
|
||||
func (r *pgRepository) DeleteMatch(serverID uuid.UUID, rawName string) error {
|
||||
normalized := strings.ToLower(strings.TrimSpace(rawName))
|
||||
return r.db.Where("raw_name = ?", normalized).Delete(&ocr.ProductMatch{}).Error
|
||||
return r.db.Where("rms_server_id = ? AND raw_name = ?", serverID, normalized).Delete(&ocr.ProductMatch{}).Error
|
||||
}
|
||||
|
||||
func (r *pgRepository) FindMatch(rawName string) (*ocr.ProductMatch, error) {
|
||||
func (r *pgRepository) FindMatch(serverID uuid.UUID, rawName string) (*ocr.ProductMatch, error) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(rawName))
|
||||
var match ocr.ProductMatch
|
||||
|
||||
// Preload Container на случай, если нам сразу нужна инфа
|
||||
err := r.db.Preload("Container").Where("raw_name = ?", normalized).First(&match).Error
|
||||
err := r.db.Preload("Container").
|
||||
Where("rms_server_id = ? AND raw_name = ?", serverID, normalized).
|
||||
First(&match).Error
|
||||
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
@@ -65,35 +76,33 @@ func (r *pgRepository) FindMatch(rawName string) (*ocr.ProductMatch, error) {
|
||||
return &match, nil
|
||||
}
|
||||
|
||||
func (r *pgRepository) GetAllMatches() ([]ocr.ProductMatch, error) {
|
||||
func (r *pgRepository) GetAllMatches(serverID uuid.UUID) ([]ocr.ProductMatch, error) {
|
||||
var matches []ocr.ProductMatch
|
||||
// Подгружаем Товар, Единицу и Фасовку
|
||||
err := r.db.
|
||||
Preload("Product").
|
||||
Preload("Product.MainUnit").
|
||||
Preload("Container").
|
||||
Where("rms_server_id = ?", serverID).
|
||||
Order("updated_at DESC").
|
||||
Find(&matches).Error
|
||||
return matches, err
|
||||
}
|
||||
|
||||
// UpsertUnmatched увеличивает счетчик встречаемости
|
||||
func (r *pgRepository) UpsertUnmatched(rawName string) error {
|
||||
func (r *pgRepository) UpsertUnmatched(serverID uuid.UUID, rawName string) error {
|
||||
normalized := strings.ToLower(strings.TrimSpace(rawName))
|
||||
if normalized == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Используем сырой SQL или GORM upsert expression
|
||||
// PostgreSQL: INSERT ... ON CONFLICT DO UPDATE SET count = count + 1
|
||||
item := ocr.UnmatchedItem{
|
||||
RawName: normalized,
|
||||
Count: 1,
|
||||
LastSeen: time.Now(),
|
||||
RMSServerID: serverID,
|
||||
RawName: normalized,
|
||||
Count: 1,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "raw_name"}},
|
||||
Columns: []clause.Column{{Name: "raw_name"}, {Name: "rms_server_id"}},
|
||||
DoUpdates: clause.Assignments(map[string]interface{}{
|
||||
"count": gorm.Expr("unmatched_items.count + 1"),
|
||||
"last_seen": time.Now(),
|
||||
@@ -101,13 +110,16 @@ func (r *pgRepository) UpsertUnmatched(rawName string) error {
|
||||
}).Create(&item).Error
|
||||
}
|
||||
|
||||
func (r *pgRepository) GetTopUnmatched(limit int) ([]ocr.UnmatchedItem, error) {
|
||||
func (r *pgRepository) GetTopUnmatched(serverID uuid.UUID, limit int) ([]ocr.UnmatchedItem, error) {
|
||||
var items []ocr.UnmatchedItem
|
||||
err := r.db.Order("count DESC, last_seen DESC").Limit(limit).Find(&items).Error
|
||||
err := r.db.Where("rms_server_id = ?", serverID).
|
||||
Order("count DESC, last_seen DESC").
|
||||
Limit(limit).
|
||||
Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (r *pgRepository) DeleteUnmatched(rawName string) error {
|
||||
func (r *pgRepository) DeleteUnmatched(serverID uuid.UUID, rawName string) error {
|
||||
normalized := strings.ToLower(strings.TrimSpace(rawName))
|
||||
return r.db.Where("raw_name = ?", normalized).Delete(&ocr.UnmatchedItem{}).Error
|
||||
return r.db.Where("rms_server_id = ? AND raw_name = ?", serverID, normalized).Delete(&ocr.UnmatchedItem{}).Error
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
|
||||
"rmser/internal/domain/operations"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -16,19 +18,15 @@ func NewRepository(db *gorm.DB) operations.Repository {
|
||||
return &pgRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *pgRepository) SaveOperations(ops []operations.StoreOperation, opType operations.OperationType, dateFrom, dateTo time.Time) error {
|
||||
func (r *pgRepository) SaveOperations(ops []operations.StoreOperation, serverID uuid.UUID, opType operations.OperationType, dateFrom, dateTo time.Time) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. Удаляем старые записи этого типа, которые пересекаются с периодом.
|
||||
// Так как отчет агрегированный, мы привязываемся к периоду "с" и "по".
|
||||
// Упрощение: удаляем всё, где PeriodFrom совпадает с текущей выгрузкой,
|
||||
// предполагая, что мы всегда грузим одними и теми же квантами (например, месяц или неделя).
|
||||
// Для надежности удалим всё, что попадает в диапазон.
|
||||
if err := tx.Where("op_type = ? AND period_from >= ? AND period_to <= ?", opType, dateFrom, dateTo).
|
||||
// Удаляем старые записи этого типа, но ТОЛЬКО для конкретного сервера
|
||||
if err := tx.Where("rms_server_id = ? AND op_type = ? AND period_from >= ? AND period_to <= ?",
|
||||
serverID, opType, dateFrom, dateTo).
|
||||
Delete(&operations.StoreOperation{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Вставляем новые
|
||||
if len(ops) > 0 {
|
||||
if err := tx.CreateInBatches(ops, 500).Error; err != nil {
|
||||
return err
|
||||
|
||||
@@ -15,6 +15,7 @@ func NewRepository(db *gorm.DB) recipes.Repository {
|
||||
return &pgRepository{db: db}
|
||||
}
|
||||
|
||||
// Техкарты сохраняются пачкой, serverID внутри структуры
|
||||
func (r *pgRepository) SaveRecipes(list []recipes.Recipe) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, recipe := range list {
|
||||
|
||||
60
internal/infrastructure/repository/suppliers/postgres.go
Normal file
60
internal/infrastructure/repository/suppliers/postgres.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package suppliers
|
||||
|
||||
import (
|
||||
"rmser/internal/domain/suppliers"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type pgRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) suppliers.Repository {
|
||||
return &pgRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *pgRepository) SaveBatch(list []suppliers.Supplier) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
UpdateAll: true,
|
||||
}).CreateInBatches(list, 100).Error
|
||||
}
|
||||
|
||||
// GetRankedByUsage возвращает поставщиков для конкретного сервера,
|
||||
// отсортированных по количеству накладных за последние N дней.
|
||||
func (r *pgRepository) GetRankedByUsage(serverID uuid.UUID, daysLookBack int) ([]suppliers.Supplier, error) {
|
||||
var result []suppliers.Supplier
|
||||
|
||||
dateThreshold := time.Now().AddDate(0, 0, -daysLookBack)
|
||||
|
||||
// SQL: Join Suppliers с Invoices, Group By Supplier, Order By Count DESC
|
||||
// Учитываем только активных поставщиков и накладные этого же сервера (через supplier_id + default_store_id косвенно,
|
||||
// но лучше явно фильтровать suppliers по rms_server_id).
|
||||
// *Примечание:* Invoices пока не имеют поля rms_server_id явно в старой схеме,
|
||||
// но мы должны фильтровать Suppliers по serverID.
|
||||
|
||||
err := r.db.Table("suppliers").
|
||||
Select("suppliers.*, COUNT(invoices.id) as usage_count").
|
||||
Joins("LEFT JOIN invoices ON invoices.supplier_id = suppliers.id AND invoices.date_incoming >= ?", dateThreshold).
|
||||
Where("suppliers.rms_server_id = ? AND suppliers.is_deleted = ?", serverID, false).
|
||||
Group("suppliers.id").
|
||||
Order("usage_count DESC, suppliers.name ASC").
|
||||
Find(&result).Error
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (r *pgRepository) Count(serverID uuid.UUID) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&suppliers.Supplier{}).
|
||||
Where("rms_server_id = ? AND is_deleted = ?", serverID, false).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
Reference in New Issue
Block a user