Add initial backend implementation

- Go module with Gin, GORM, JWT, excelize dependencies
- Configuration loading from YAML with all settings
- GORM models for users, categories, components, configurations, alerts
- Repository layer for all entities
- Services: auth (JWT), pricing (median/average/weighted), components,
  quotes, configurations, export (CSV/XLSX), alerts
- Middleware: JWT auth, role-based access, CORS
- HTTP handlers for all API endpoints
- Main server with dependency injection and graceful shutdown

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-01-23 10:49:56 +03:00
parent a4ab83d4d9
commit 190a9aa0a3
36 changed files with 4293 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
package repository
import (
"time"
"github.com/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
}