Улучшения управления ценами и конфигурациями

- Добавлено отображение последней полученной цены в окне настройки цены
- Добавлен функционал переименования конфигураций (PATCH /api/configs/:uuid/rename)
- Изменён формат имени файла при экспорте: "YYYY-MM-DD NAME SPEC.ext"
- Исправлена сортировка компонентов: перенесена на сервер для корректной работы с пагинацией
- Добавлен расчёт popularity_score на основе котировок из lot_log
- Исправлена потеря настроек (метод, период, коэффициент) при пересчёте цен

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-01-27 11:39:12 +03:00
parent d7d6e9d62c
commit 7ded78f2c3
9 changed files with 269 additions and 73 deletions

View File

@@ -16,9 +16,11 @@ func NewComponentRepository(db *gorm.DB) *ComponentRepository {
}
type ComponentFilter struct {
Category string
Search string
HasPrice bool
Category string
Search string
HasPrice bool
SortField string
SortDir string
}
func (r *ComponentRepository) List(filter ComponentFilter, offset, limit int) ([]models.LotMetadata, int64, error) {
@@ -43,10 +45,33 @@ func (r *ComponentRepository) List(filter ComponentFilter, offset, limit int) ([
query.Count(&total)
// Sort by popularity + freshness, no price goes last
// Apply sorting
sortDir := "ASC"
if filter.SortDir == "desc" {
sortDir = "DESC"
}
switch filter.SortField {
case "popularity_score":
query = query.Order("popularity_score " + sortDir)
case "current_price":
query = query.Order("CASE WHEN current_price IS NULL OR current_price = 0 THEN 1 ELSE 0 END").
Order("current_price " + sortDir)
case "lot_name":
query = query.Order("lot_name " + sortDir)
case "quote_count":
// Sort by quote count from lot_log table
query = query.
Select("qt_lot_metadata.*, (SELECT COUNT(*) FROM lot_log WHERE lot_log.lot = qt_lot_metadata.lot_name) as quote_count_sort").
Order("quote_count_sort " + sortDir)
default:
// Default: sort by popularity, no price goes last
query = query.
Order("CASE WHEN current_price IS NULL OR current_price = 0 THEN 1 ELSE 0 END").
Order("popularity_score DESC")
}
err := query.
Order("CASE WHEN current_price IS NULL OR current_price = 0 THEN 1 ELSE 0 END").
Order("popularity_score DESC").
Offset(offset).
Limit(limit).
Find(&components).Error

View File

@@ -90,3 +90,26 @@ func (r *StatsRepository) ResetMonthlyCounters() error {
Where("1 = 1").
Update("quotes_last_30d", 0).Error
}
// UpdatePopularityScores recalculates popularity_score in qt_lot_metadata
// based on supplier quotes from lot_log table
func (r *StatsRepository) UpdatePopularityScores() error {
// Formula: popularity_score = quotes_last_30d * 3 + quotes_last_90d * 1 + quotes_total * 0.1
// This gives more weight to recent supplier activity
return r.db.Exec(`
UPDATE qt_lot_metadata m
LEFT JOIN (
SELECT
lot,
COUNT(*) as quotes_total,
SUM(CASE WHEN date >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 ELSE 0 END) as quotes_last_30d,
SUM(CASE WHEN date >= DATE_SUB(NOW(), INTERVAL 90 DAY) THEN 1 ELSE 0 END) as quotes_last_90d
FROM lot_log
GROUP BY lot
) s ON m.lot_name = s.lot
SET m.popularity_score = COALESCE(
s.quotes_last_30d * 3 + s.quotes_last_90d * 1 + s.quotes_total * 0.1,
0
)
`).Error
}