feat: сохранять ручные PN→LOT маппинги как lot_suggestion в qt_vendor_partnumber_seen
При сохранении 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>
This commit is contained in:
@@ -1,20 +1,31 @@
|
||||
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
|
||||
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.
|
||||
// Existing rows are left untouched: no updates to last_seen_at, is_ignored, or description.
|
||||
// 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
|
||||
@@ -30,7 +41,43 @@ func (s *Service) PushPartnumberSeen(items []SeenPartnumber) error {
|
||||
if item.Partnumber == "" {
|
||||
continue
|
||||
}
|
||||
err := mariaDB.Exec(`
|
||||
|
||||
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
|
||||
@@ -40,10 +87,18 @@ func (s *Service) PushPartnumberSeen(items []SeenPartnumber) error {
|
||||
`, item.Partnumber, item.Description, item.Ignored, now).Error
|
||||
if err != nil {
|
||||
slog.Error("failed to insert partnumber_seen", "partnumber", item.Partnumber, "error", err)
|
||||
// Continue with remaining items
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user