Добавлены сортировка по категориям, секции PCI и автосохранение
Основные изменения: 1. CSV экспорт и веб-интерфейс: - Компоненты теперь сортируются по иерархии категорий (display_order) - Категории отображаются в правильном порядке: BB, CPU, MEM, GPU и т.д. - Компоненты без категории отображаются в конце 2. Раздел PCI в конфигураторе: - Разделен на секции: GPU/DPU, NIC/HCA, HBA - Улучшена навигация и выбор компонентов 3. Сохранение "своей цены": - Добавлено поле custom_price в модель Configuration - Создана миграция 002_add_custom_price.sql - "Своя цена" сохраняется при сохранении конфигурации - При загрузке конфигурации восстанавливается сохраненная цена 4. Автосохранение: - Конфигурация автоматически сохраняется через 1 секунду после изменений - Debounce предотвращает избыточные запросы - Автосохранение работает для всех изменений (компоненты, количество, цена) 5. Дополнительно: - Добавлен cmd/importer для импорта метаданных из таблицы lot - Создан скрипт apply_migration.sh для применения миграций - Оптимизирована работа с категориями в ExportService Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
160
cmd/importer/main.go
Normal file
160
cmd/importer/main.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/quoteforge/internal/config"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "config.yaml", "path to config file")
|
||||
flag.Parse()
|
||||
|
||||
cfg, err := config.Load(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
db, err := gorm.Open(mysql.Open(cfg.Database.DSN()), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Connected to database")
|
||||
|
||||
// Ensure tables exist
|
||||
if err := models.Migrate(db); err != nil {
|
||||
log.Fatalf("Migration failed: %v", err)
|
||||
}
|
||||
if err := models.SeedCategories(db); err != nil {
|
||||
log.Fatalf("Seeding categories failed: %v", err)
|
||||
}
|
||||
|
||||
// Load categories for lookup
|
||||
var categories []models.Category
|
||||
db.Find(&categories)
|
||||
categoryMap := make(map[string]uint)
|
||||
for _, c := range categories {
|
||||
categoryMap[c.Code] = c.ID
|
||||
}
|
||||
log.Printf("Loaded %d categories", len(categories))
|
||||
|
||||
// Get all lots
|
||||
var lots []models.Lot
|
||||
if err := db.Find(&lots).Error; err != nil {
|
||||
log.Fatalf("Failed to load lots: %v", err)
|
||||
}
|
||||
log.Printf("Found %d lots to import", len(lots))
|
||||
|
||||
// Import each lot
|
||||
var imported, skipped, updated int
|
||||
for _, lot := range lots {
|
||||
category, model := ParsePartNumber(lot.LotName)
|
||||
|
||||
var categoryID *uint
|
||||
if id, ok := categoryMap[category]; ok && id > 0 {
|
||||
categoryID = &id
|
||||
} else {
|
||||
// Try to find by prefix match
|
||||
for code, id := range categoryMap {
|
||||
if strings.HasPrefix(category, code) {
|
||||
categoryID = &id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if already exists
|
||||
var existing models.LotMetadata
|
||||
result := db.Where("lot_name = ?", lot.LotName).First(&existing)
|
||||
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
// Check if there are prices in the last 90 days
|
||||
var recentPriceCount int64
|
||||
db.Model(&models.LotLog{}).
|
||||
Where("lot = ? AND date >= DATE_SUB(NOW(), INTERVAL 90 DAY)", lot.LotName).
|
||||
Count(&recentPriceCount)
|
||||
|
||||
// Default to 90 days, but use "all time" (0) if no recent prices
|
||||
periodDays := 90
|
||||
if recentPriceCount == 0 {
|
||||
periodDays = 0
|
||||
}
|
||||
|
||||
// Create new
|
||||
metadata := models.LotMetadata{
|
||||
LotName: lot.LotName,
|
||||
CategoryID: categoryID,
|
||||
Model: model,
|
||||
PricePeriodDays: periodDays,
|
||||
}
|
||||
if err := db.Create(&metadata).Error; err != nil {
|
||||
log.Printf("Failed to create metadata for %s: %v", lot.LotName, err)
|
||||
continue
|
||||
}
|
||||
imported++
|
||||
} else if result.Error == nil {
|
||||
// Update if needed
|
||||
needsUpdate := false
|
||||
|
||||
if existing.Model == "" {
|
||||
existing.Model = model
|
||||
needsUpdate = true
|
||||
}
|
||||
if existing.CategoryID == nil {
|
||||
existing.CategoryID = categoryID
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
// Check if using default period (90 days) but no recent prices
|
||||
if existing.PricePeriodDays == 90 {
|
||||
var recentPriceCount int64
|
||||
db.Model(&models.LotLog{}).
|
||||
Where("lot = ? AND date >= DATE_SUB(NOW(), INTERVAL 90 DAY)", lot.LotName).
|
||||
Count(&recentPriceCount)
|
||||
|
||||
if recentPriceCount == 0 {
|
||||
existing.PricePeriodDays = 0
|
||||
needsUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
if needsUpdate {
|
||||
db.Save(&existing)
|
||||
updated++
|
||||
} else {
|
||||
skipped++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Import complete: %d imported, %d updated, %d skipped", imported, updated, skipped)
|
||||
|
||||
// Show final counts
|
||||
var metadataCount int64
|
||||
db.Model(&models.LotMetadata{}).Count(&metadataCount)
|
||||
log.Printf("Total metadata records: %d", metadataCount)
|
||||
}
|
||||
|
||||
// ParsePartNumber extracts category and model from lot_name
|
||||
// Examples:
|
||||
// "CPU_AMD_9654" → category="CPU", model="AMD_9654"
|
||||
// "MB_INTEL_4.Sapphire_2S" → category="MB", model="INTEL_4.Sapphire_2S"
|
||||
func ParsePartNumber(lotName string) (category, model string) {
|
||||
parts := strings.SplitN(lotName, "_", 2)
|
||||
if len(parts) >= 1 {
|
||||
category = parts[0]
|
||||
}
|
||||
if len(parts) >= 2 {
|
||||
model = parts[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -170,7 +170,7 @@ func setupRouter(db *gorm.DB, cfg *config.Config) (*gin.Engine, error) {
|
||||
componentService := services.NewComponentService(componentRepo, categoryRepo, statsRepo)
|
||||
quoteService := services.NewQuoteService(componentRepo, statsRepo, pricingService)
|
||||
configService := services.NewConfigurationService(configRepo, componentRepo, quoteService)
|
||||
exportService := services.NewExportService(cfg.Export)
|
||||
exportService := services.NewExportService(cfg.Export, categoryRepo)
|
||||
alertService := alerts.NewService(alertRepo, componentRepo, priceRepo, statsRepo, cfg.Alerts, cfg.Pricing)
|
||||
|
||||
// Handlers
|
||||
@@ -294,10 +294,11 @@ func setupRouter(db *gorm.DB, cfg *config.Config) (*gin.Engine, error) {
|
||||
configs.GET("/:uuid", configHandler.Get)
|
||||
configs.PUT("/:uuid", configHandler.Update)
|
||||
configs.PATCH("/:uuid/rename", configHandler.Rename)
|
||||
configs.POST("/:uuid/clone", configHandler.Clone)
|
||||
configs.DELETE("/:uuid", configHandler.Delete)
|
||||
configs.GET("/:uuid/export", configHandler.ExportJSON)
|
||||
// configs.GET("/:uuid/export", configHandler.ExportJSON)
|
||||
configs.GET("/:uuid/csv", exportHandler.ExportConfigCSV)
|
||||
configs.POST("/import", configHandler.ImportJSON)
|
||||
// configs.POST("/import", configHandler.ImportJSON)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user