fix: потоковая отправка прогресса создания прайслиста и исправление маппинга колонки категории

Две ключевые исправления:

1. Потоковая отправка прогресса создания (SSE):
   - Эндпоинт CreateWithProgress теперь отправляет Server-Sent Events
     вместо возврата JSON с task_id
   - Полирует статус задачи и отправляет обновления прогресса в реальном времени
   - Отправляет финальное событие с данными прайслиста или ошибкой
   - Фронтенд уже ожидал этого формата SSE

2. Исправление маппинга колонки lot_category:
   - Добавлен явный тег column в поле Category модели PricelistItem
     чтобы маппиться на колонку 'lot_category' в БД
   - Категория теперь хранится как снимок в таблице pricelist_items
   - Обновлены запросы репозитория для использования сохраненной
     категории вместо динамических JOIN с таблицей lot

Это исправляет ошибки:
- "Создание прервано: не получен результат" (фронтенд ожидал streaming)
- "Unknown column 'category' in 'INSERT INTO'" (несоответствие схемы БД)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-02-10 15:17:16 +03:00
parent 7d671203d7
commit c47c93ab31
6 changed files with 148 additions and 11 deletions

View File

@@ -35,6 +35,7 @@ type CreateItemInput struct {
LotName string
Price float64
PriceMethod string
Category string
}
func NewService(db *gorm.DB, repo *repository.PricelistRepository, componentRepo *repository.ComponentRepository, pricingSvc *pricing.Service) *Service {
@@ -170,28 +171,63 @@ func (s *Service) CreateForSourceWithProgress(createdBy, source string, sourceIt
LotName: item.LotName,
Price: item.Price,
PriceMethod: item.PriceMethod,
Category: item.Category,
})
}
}
if len(sourceItems) > 0 {
// Load categories for all lot names if not provided
lotNamesToLoad := make([]string, 0)
for _, srcItem := range sourceItems {
if strings.TrimSpace(srcItem.LotName) != "" && srcItem.Price > 0 && srcItem.Category == "" {
lotNamesToLoad = append(lotNamesToLoad, strings.TrimSpace(srcItem.LotName))
}
}
categoryByLot := make(map[string]string)
if len(lotNamesToLoad) > 0 {
var lotCategories []struct {
LotName string
LotCategory string
}
if err := s.db.Table("lot").Select("lot_name, lot_category").Where("lot_name IN ?", lotNamesToLoad).Scan(&lotCategories).Error; err == nil {
for _, lc := range lotCategories {
categoryByLot[lc.LotName] = lc.LotCategory
}
}
}
items = make([]models.PricelistItem, 0, len(sourceItems))
for _, srcItem := range sourceItems {
if strings.TrimSpace(srcItem.LotName) == "" || srcItem.Price <= 0 {
continue
}
category := srcItem.Category
if category == "" {
category = categoryByLot[strings.TrimSpace(srcItem.LotName)]
}
items = append(items, models.PricelistItem{
PricelistID: pricelist.ID,
LotName: strings.TrimSpace(srcItem.LotName),
Price: srcItem.Price,
PriceMethod: strings.TrimSpace(srcItem.PriceMethod),
Category: category,
})
}
} else {
// Default snapshot source for estimate and backward compatibility.
var metadata []models.LotMetadata
if err := s.db.Where("current_price IS NOT NULL AND current_price > 0").Find(&metadata).Error; err != nil {
return nil, fmt.Errorf("getting lot metadata: %w", err)
type LotMetadataWithCategory struct {
models.LotMetadata
LotCategory string
}
var metadata []LotMetadataWithCategory
if err := s.db.Table("qt_lot_metadata as m").
Select("m.*, COALESCE(l.lot_category, '') as lot_category").
Joins("LEFT JOIN lot as l ON l.lot_name = m.lot_name").
Where("m.current_price IS NOT NULL AND m.current_price > 0").
Scan(&metadata).Error; err != nil {
return nil, fmt.Errorf("getting lot metadata with categories: %w", err)
}
// Create pricelist items with all price settings
@@ -209,6 +245,7 @@ func (s *Service) CreateForSourceWithProgress(createdBy, source string, sourceIt
PriceCoefficient: m.PriceCoefficient,
ManualPrice: m.ManualPrice,
MetaPrices: m.MetaPrices,
Category: m.LotCategory,
})
}
}