При сохранении vendor-spec строки с заполненным lot_mappings автоматически
отправляются на сервер и пишутся в новый столбец lot_suggestion. Столбец
хранит JSON-массив [{lot_name, qty}] — тот же формат, что qt_partnumber_book_items.lots_json.
Если миграция ещё не прошла (столбец отсутствует), приложение логирует WARN
и записывает строку без столбца; сбоя нет.
Контракт для инструмента создания partnumber-books описан в bible-local/11-lot-suggestions.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
105 lines
3.2 KiB
Go
105 lines
3.2 KiB
Go
package sync
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SeenPartnumber represents an unresolved vendor partnumber to report.
|
|
type SeenPartnumber struct {
|
|
Partnumber string
|
|
Description string
|
|
Ignored bool
|
|
LotSuggestion []LotSuggestionEntry // optional; set when user manually mapped PN → LOT in UI
|
|
}
|
|
|
|
// LotSuggestionEntry is one suggested LOT mapping for a vendor partnumber.
|
|
// JSON shape mirrors qt_partnumber_book_items.lots_json: {"lot_name", "qty"}.
|
|
type LotSuggestionEntry struct {
|
|
LotName string `json:"lot_name"`
|
|
Qty int `json:"qty"`
|
|
}
|
|
|
|
// PushPartnumberSeen inserts unresolved vendor partnumbers into qt_vendor_partnumber_seen on MariaDB.
|
|
// When LotSuggestion is provided the column is updated too; if the column does not exist yet
|
|
// (migration pending) the write is retried without it and a warning is logged — the app never panics.
|
|
func (s *Service) PushPartnumberSeen(items []SeenPartnumber) error {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
mariaDB, err := s.getDB()
|
|
if err != nil {
|
|
return fmt.Errorf("database not available: %w", err)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
for _, item := range items {
|
|
if item.Partnumber == "" {
|
|
continue
|
|
}
|
|
|
|
if len(item.LotSuggestion) > 0 {
|
|
suggJSON, marshalErr := json.Marshal(item.LotSuggestion)
|
|
if marshalErr != nil {
|
|
slog.Error("partnumber_seen: failed to marshal lot_suggestion, skipping suggestion",
|
|
"partnumber", item.Partnumber, "error", marshalErr)
|
|
suggJSON = nil
|
|
}
|
|
|
|
if suggJSON != nil {
|
|
err = mariaDB.Exec(`
|
|
INSERT INTO qt_vendor_partnumber_seen
|
|
(source_type, vendor, partnumber, description, is_ignored, last_seen_at, lot_suggestion)
|
|
VALUES
|
|
('manual', '', ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
lot_suggestion = VALUES(lot_suggestion),
|
|
last_seen_at = NOW(3)
|
|
`, item.Partnumber, item.Description, item.Ignored, now, string(suggJSON)).Error
|
|
|
|
if err == nil {
|
|
continue
|
|
}
|
|
|
|
// Column not yet migrated — fall through to insert without lot_suggestion.
|
|
if !isUnknownColumnError(err) {
|
|
slog.Error("partnumber_seen: failed to upsert with lot_suggestion",
|
|
"partnumber", item.Partnumber, "error", err)
|
|
continue
|
|
}
|
|
slog.Warn("partnumber_seen: lot_suggestion column missing (migration pending), inserting without it",
|
|
"partnumber", item.Partnumber)
|
|
}
|
|
}
|
|
|
|
// Insert without lot_suggestion (baseline behaviour or fallback).
|
|
err = mariaDB.Exec(`
|
|
INSERT INTO qt_vendor_partnumber_seen
|
|
(source_type, vendor, partnumber, description, is_ignored, last_seen_at)
|
|
VALUES
|
|
('manual', '', ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
partnumber = partnumber
|
|
`, item.Partnumber, item.Description, item.Ignored, now).Error
|
|
if err != nil {
|
|
slog.Error("failed to insert partnumber_seen", "partnumber", item.Partnumber, "error", err)
|
|
}
|
|
}
|
|
|
|
slog.Info("partnumber_seen pushed to server", "count", len(items))
|
|
return nil
|
|
}
|
|
|
|
// isUnknownColumnError returns true when MariaDB reports that a column does not exist.
|
|
func isUnknownColumnError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
msg := strings.ToLower(err.Error())
|
|
return strings.Contains(msg, "unknown column") || strings.Contains(msg, "1054")
|
|
}
|