330 lines
9.0 KiB
Go
330 lines
9.0 KiB
Go
package localdb
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ComponentFilter for searching with filters
|
|
type ComponentFilter struct {
|
|
Category string
|
|
Search string
|
|
HasPrice bool
|
|
}
|
|
|
|
// ComponentSyncResult contains statistics from component sync
|
|
type ComponentSyncResult struct {
|
|
TotalSynced int
|
|
NewCount int
|
|
UpdateCount int
|
|
Duration time.Duration
|
|
}
|
|
|
|
// SyncComponents loads components from MariaDB (lot + qt_lot_metadata) into local_components
|
|
func (l *LocalDB) SyncComponents(mariaDB *gorm.DB) (*ComponentSyncResult, error) {
|
|
startTime := time.Now()
|
|
|
|
// Query to join lot with qt_lot_metadata (metadata only, no pricing)
|
|
// Use LEFT JOIN to include lots without metadata
|
|
type componentRow struct {
|
|
LotName string
|
|
LotDescription string
|
|
Category *string
|
|
Model *string
|
|
}
|
|
|
|
var rows []componentRow
|
|
err := mariaDB.Raw(`
|
|
SELECT
|
|
l.lot_name,
|
|
l.lot_description,
|
|
COALESCE(c.code, SUBSTRING_INDEX(l.lot_name, '_', 1)) as category,
|
|
m.model
|
|
FROM lot l
|
|
LEFT JOIN qt_lot_metadata m ON l.lot_name = m.lot_name
|
|
LEFT JOIN qt_categories c ON m.category_id = c.id
|
|
WHERE m.is_hidden = FALSE OR m.is_hidden IS NULL
|
|
ORDER BY l.lot_name
|
|
`).Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, fmt.Errorf("querying components from MariaDB: %w", err)
|
|
}
|
|
|
|
if len(rows) == 0 {
|
|
slog.Warn("no components found in MariaDB")
|
|
return &ComponentSyncResult{
|
|
Duration: time.Since(startTime),
|
|
}, nil
|
|
}
|
|
|
|
// Get existing local components for comparison
|
|
existingMap := make(map[string]bool)
|
|
var existing []LocalComponent
|
|
if err := l.db.Find(&existing).Error; err != nil {
|
|
return nil, fmt.Errorf("reading existing local components: %w", err)
|
|
}
|
|
for _, c := range existing {
|
|
existingMap[c.LotName] = true
|
|
}
|
|
|
|
// Prepare components for batch insert/update
|
|
syncTime := time.Now()
|
|
components := make([]LocalComponent, 0, len(rows))
|
|
newCount := 0
|
|
|
|
for _, row := range rows {
|
|
category := ""
|
|
if row.Category != nil {
|
|
category = *row.Category
|
|
} else {
|
|
// Parse category from lot_name (e.g., "CPU_AMD_9654" -> "CPU")
|
|
parts := strings.SplitN(row.LotName, "_", 2)
|
|
if len(parts) >= 1 {
|
|
category = parts[0]
|
|
}
|
|
}
|
|
|
|
model := ""
|
|
if row.Model != nil {
|
|
model = *row.Model
|
|
}
|
|
|
|
comp := LocalComponent{
|
|
LotName: row.LotName,
|
|
LotDescription: row.LotDescription,
|
|
Category: category,
|
|
Model: model,
|
|
}
|
|
components = append(components, comp)
|
|
|
|
if !existingMap[row.LotName] {
|
|
newCount++
|
|
}
|
|
}
|
|
|
|
// Use transaction for bulk upsert
|
|
err = l.db.Transaction(func(tx *gorm.DB) error {
|
|
// Delete all existing and insert new (simpler than upsert for SQLite)
|
|
if err := tx.Where("1=1").Delete(&LocalComponent{}).Error; err != nil {
|
|
return fmt.Errorf("clearing local components: %w", err)
|
|
}
|
|
|
|
// Batch insert
|
|
batchSize := 500
|
|
for i := 0; i < len(components); i += batchSize {
|
|
end := i + batchSize
|
|
if end > len(components) {
|
|
end = len(components)
|
|
}
|
|
if err := tx.CreateInBatches(components[i:end], batchSize).Error; err != nil {
|
|
return fmt.Errorf("inserting components batch: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update last sync time
|
|
if err := l.SetComponentSyncTime(syncTime); err != nil {
|
|
slog.Warn("failed to update component sync time", "error", err)
|
|
}
|
|
|
|
result := &ComponentSyncResult{
|
|
TotalSynced: len(components),
|
|
NewCount: newCount,
|
|
UpdateCount: len(components) - newCount,
|
|
Duration: time.Since(startTime),
|
|
}
|
|
|
|
slog.Info("components synced",
|
|
"total", result.TotalSynced,
|
|
"new", result.NewCount,
|
|
"updated", result.UpdateCount,
|
|
"duration", result.Duration)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// SearchLocalComponents searches components in local cache by query string
|
|
// Searches in lot_name, lot_description, category, and model fields
|
|
func (l *LocalDB) SearchLocalComponents(query string, limit int) ([]LocalComponent, error) {
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
|
|
var components []LocalComponent
|
|
|
|
if query == "" {
|
|
// Return all components with limit
|
|
err := l.db.Order("lot_name").Limit(limit).Find(&components).Error
|
|
return components, err
|
|
}
|
|
|
|
// Search with LIKE on multiple fields
|
|
searchPattern := "%" + strings.ToLower(query) + "%"
|
|
|
|
err := l.db.Where(
|
|
"LOWER(lot_name) LIKE ? OR LOWER(lot_description) LIKE ? OR LOWER(category) LIKE ? OR LOWER(model) LIKE ?",
|
|
searchPattern, searchPattern, searchPattern, searchPattern,
|
|
).Order("lot_name").Limit(limit).Find(&components).Error
|
|
|
|
return components, err
|
|
}
|
|
|
|
// SearchLocalComponentsByCategory searches components by category and optional query
|
|
func (l *LocalDB) SearchLocalComponentsByCategory(category string, query string, limit int) ([]LocalComponent, error) {
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
|
|
var components []LocalComponent
|
|
db := l.db.Where("LOWER(category) = ?", strings.ToLower(category))
|
|
|
|
if query != "" {
|
|
searchPattern := "%" + strings.ToLower(query) + "%"
|
|
db = db.Where(
|
|
"LOWER(lot_name) LIKE ? OR LOWER(lot_description) LIKE ? OR LOWER(model) LIKE ?",
|
|
searchPattern, searchPattern, searchPattern,
|
|
)
|
|
}
|
|
|
|
err := db.Order("lot_name").Limit(limit).Find(&components).Error
|
|
return components, err
|
|
}
|
|
|
|
// ListComponents returns components with filtering and pagination
|
|
func (l *LocalDB) ListComponents(filter ComponentFilter, offset, limit int) ([]LocalComponent, int64, error) {
|
|
db := l.db
|
|
|
|
// Apply category filter
|
|
if filter.Category != "" {
|
|
db = db.Where("LOWER(category) = ?", strings.ToLower(filter.Category))
|
|
}
|
|
|
|
// Apply search filter
|
|
if filter.Search != "" {
|
|
searchPattern := "%" + strings.ToLower(filter.Search) + "%"
|
|
db = db.Where(
|
|
"LOWER(lot_name) LIKE ? OR LOWER(lot_description) LIKE ? OR LOWER(category) LIKE ? OR LOWER(model) LIKE ?",
|
|
searchPattern, searchPattern, searchPattern, searchPattern,
|
|
)
|
|
}
|
|
|
|
// Get total count
|
|
var total int64
|
|
if err := db.Model(&LocalComponent{}).Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Apply pagination and get results
|
|
var components []LocalComponent
|
|
if err := db.Order("lot_name").Offset(offset).Limit(limit).Find(&components).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return components, total, nil
|
|
}
|
|
|
|
// GetLocalComponent returns a single component by lot_name
|
|
func (l *LocalDB) GetLocalComponent(lotName string) (*LocalComponent, error) {
|
|
var component LocalComponent
|
|
err := l.db.Where("lot_name = ?", lotName).First(&component).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &component, nil
|
|
}
|
|
|
|
// GetLocalComponentCategoriesByLotNames returns category for each lot_name in the local component cache.
|
|
// Missing lots are not included in the map; caller is responsible for strict validation.
|
|
func (l *LocalDB) GetLocalComponentCategoriesByLotNames(lotNames []string) (map[string]string, error) {
|
|
result := make(map[string]string, len(lotNames))
|
|
if len(lotNames) == 0 {
|
|
return result, nil
|
|
}
|
|
|
|
type row struct {
|
|
LotName string `gorm:"column:lot_name"`
|
|
Category string `gorm:"column:category"`
|
|
}
|
|
var rows []row
|
|
if err := l.db.Model(&LocalComponent{}).
|
|
Select("lot_name, category").
|
|
Where("lot_name IN ?", lotNames).
|
|
Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
for _, r := range rows {
|
|
result[r.LotName] = r.Category
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// GetLocalComponentCategories returns distinct categories from local components
|
|
func (l *LocalDB) GetLocalComponentCategories() ([]string, error) {
|
|
var categories []string
|
|
err := l.db.Model(&LocalComponent{}).
|
|
Distinct("category").
|
|
Where("category != ''").
|
|
Order("category").
|
|
Pluck("category", &categories).Error
|
|
return categories, err
|
|
}
|
|
|
|
// CountLocalComponents returns the total number of local components
|
|
func (l *LocalDB) CountLocalComponents() int64 {
|
|
var count int64
|
|
l.db.Model(&LocalComponent{}).Count(&count)
|
|
return count
|
|
}
|
|
|
|
// CountLocalComponentsByCategory returns component count by category
|
|
func (l *LocalDB) CountLocalComponentsByCategory(category string) int64 {
|
|
var count int64
|
|
l.db.Model(&LocalComponent{}).Where("LOWER(category) = ?", strings.ToLower(category)).Count(&count)
|
|
return count
|
|
}
|
|
|
|
// GetComponentSyncTime returns the last component sync timestamp
|
|
func (l *LocalDB) GetComponentSyncTime() *time.Time {
|
|
var setting struct {
|
|
Value string
|
|
}
|
|
if err := l.db.Table("app_settings").
|
|
Where("key = ?", "last_component_sync").
|
|
First(&setting).Error; err != nil {
|
|
return nil
|
|
}
|
|
|
|
t, err := time.Parse(time.RFC3339, setting.Value)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &t
|
|
}
|
|
|
|
// SetComponentSyncTime sets the last component sync timestamp
|
|
func (l *LocalDB) SetComponentSyncTime(t time.Time) error {
|
|
return l.db.Exec(`
|
|
INSERT INTO app_settings (key, value, updated_at)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
`, "last_component_sync", t.Format(time.RFC3339), time.Now().Format(time.RFC3339)).Error
|
|
}
|
|
|
|
// NeedComponentSync checks if component sync is needed (older than specified hours)
|
|
func (l *LocalDB) NeedComponentSync(maxAgeHours int) bool {
|
|
syncTime := l.GetComponentSyncTime()
|
|
if syncTime == nil {
|
|
return true
|
|
}
|
|
return time.Since(*syncTime).Hours() > float64(maxAgeHours)
|
|
}
|