Files
Mikhail Chusavitin 7ded78f2c3 Улучшения управления ценами и конфигурациями
- Добавлено отображение последней полученной цены в окне настройки цены
- Добавлен функционал переименования конфигураций (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>
2026-01-27 11:39:12 +03:00

116 lines
3.3 KiB
Go

package repository
import (
"time"
"git.mchus.pro/mchus/quoteforge/internal/models"
"gorm.io/gorm"
)
type StatsRepository struct {
db *gorm.DB
}
func NewStatsRepository(db *gorm.DB) *StatsRepository {
return &StatsRepository{db: db}
}
func (r *StatsRepository) GetByLotName(lotName string) (*models.ComponentUsageStats, error) {
var stats models.ComponentUsageStats
err := r.db.Where("lot_name = ?", lotName).First(&stats).Error
if err != nil {
return nil, err
}
return &stats, nil
}
func (r *StatsRepository) Upsert(stats *models.ComponentUsageStats) error {
return r.db.Save(stats).Error
}
func (r *StatsRepository) IncrementUsage(lotName string, quantity int, revenue float64) error {
now := time.Now()
result := r.db.Model(&models.ComponentUsageStats{}).
Where("lot_name = ?", lotName).
Updates(map[string]interface{}{
"quotes_total": gorm.Expr("quotes_total + 1"),
"quotes_last_30d": gorm.Expr("quotes_last_30d + 1"),
"quotes_last_7d": gorm.Expr("quotes_last_7d + 1"),
"total_quantity": gorm.Expr("total_quantity + ?", quantity),
"total_revenue": gorm.Expr("total_revenue + ?", revenue),
"last_used_at": now,
})
if result.RowsAffected == 0 {
stats := &models.ComponentUsageStats{
LotName: lotName,
QuotesTotal: 1,
QuotesLast30d: 1,
QuotesLast7d: 1,
TotalQuantity: quantity,
TotalRevenue: revenue,
LastUsedAt: &now,
}
return r.db.Create(stats).Error
}
return result.Error
}
func (r *StatsRepository) GetTopComponents(limit int) ([]models.ComponentUsageStats, error) {
var stats []models.ComponentUsageStats
err := r.db.
Order("quotes_last_30d DESC").
Limit(limit).
Find(&stats).Error
return stats, err
}
func (r *StatsRepository) GetTrendingComponents(limit int) ([]models.ComponentUsageStats, error) {
var stats []models.ComponentUsageStats
err := r.db.
Where("trend_direction = ? AND trend_percent > ?", models.TrendUp, 20).
Order("trend_percent DESC").
Limit(limit).
Find(&stats).Error
return stats, err
}
// ResetWeeklyCounters resets quotes_last_7d (run weekly via cron)
func (r *StatsRepository) ResetWeeklyCounters() error {
return r.db.Model(&models.ComponentUsageStats{}).
Where("1 = 1").
Update("quotes_last_7d", 0).Error
}
// ResetMonthlyCounters resets quotes_last_30d (run monthly via cron)
func (r *StatsRepository) ResetMonthlyCounters() error {
return r.db.Model(&models.ComponentUsageStats{}).
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
}