fix: prevent duplicate partnumbers in stock mappings table
Add duplicate detection before batch insert of auto-mappings: - Query existing partnumbers from lot_partnumbers table - Build case-insensitive set of existing entries - Filter out duplicates before CreateInBatches - Also prevent duplicates within single batch This ensures partnumber uniqueness as primary key requires. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
238
internal/lotmatch_old/resolver.go
Normal file
238
internal/lotmatch_old/resolver.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package lotmatch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/priceforge/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrResolveConflict = errors.New("multiple lot matches")
|
||||
ErrResolveNotFound = errors.New("lot not found")
|
||||
)
|
||||
|
||||
type LotResolver struct {
|
||||
partnumberToLots map[string][]string
|
||||
exactLots map[string]string
|
||||
allLots []string
|
||||
}
|
||||
|
||||
type MappingMatcher struct {
|
||||
exact map[string][]string
|
||||
exactLot map[string]string
|
||||
wildcard []wildcardMapping
|
||||
}
|
||||
|
||||
type wildcardMapping struct {
|
||||
lotName string
|
||||
re *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewLotResolverFromDB(db *gorm.DB) (*LotResolver, error) {
|
||||
mappings, lots, err := loadMappingsAndLots(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewLotResolver(mappings, lots), nil
|
||||
}
|
||||
|
||||
func NewMappingMatcherFromDB(db *gorm.DB) (*MappingMatcher, error) {
|
||||
mappings, lots, err := loadMappingsAndLots(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewMappingMatcher(mappings, lots), nil
|
||||
}
|
||||
|
||||
func NewLotResolver(mappings []models.LotPartnumber, lots []models.Lot) *LotResolver {
|
||||
partnumberToLots := make(map[string][]string, len(mappings))
|
||||
for _, m := range mappings {
|
||||
pn := NormalizeKey(m.Partnumber)
|
||||
lot := strings.TrimSpace(m.LotName)
|
||||
if pn == "" || lot == "" {
|
||||
continue
|
||||
}
|
||||
partnumberToLots[pn] = append(partnumberToLots[pn], lot)
|
||||
}
|
||||
for key := range partnumberToLots {
|
||||
partnumberToLots[key] = uniqueCaseInsensitive(partnumberToLots[key])
|
||||
}
|
||||
|
||||
exactLots := make(map[string]string, len(lots))
|
||||
allLots := make([]string, 0, len(lots))
|
||||
for _, l := range lots {
|
||||
name := strings.TrimSpace(l.LotName)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
exactLots[NormalizeKey(name)] = name
|
||||
allLots = append(allLots, name)
|
||||
}
|
||||
sort.Slice(allLots, func(i, j int) bool {
|
||||
li := len([]rune(allLots[i]))
|
||||
lj := len([]rune(allLots[j]))
|
||||
if li == lj {
|
||||
return allLots[i] < allLots[j]
|
||||
}
|
||||
return li > lj
|
||||
})
|
||||
|
||||
return &LotResolver{
|
||||
partnumberToLots: partnumberToLots,
|
||||
exactLots: exactLots,
|
||||
allLots: allLots,
|
||||
}
|
||||
}
|
||||
|
||||
func NewMappingMatcher(mappings []models.LotPartnumber, lots []models.Lot) *MappingMatcher {
|
||||
exact := make(map[string][]string, len(mappings))
|
||||
wildcards := make([]wildcardMapping, 0, len(mappings))
|
||||
for _, m := range mappings {
|
||||
pn := NormalizeKey(m.Partnumber)
|
||||
lot := strings.TrimSpace(m.LotName)
|
||||
if pn == "" || lot == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(pn, "*") {
|
||||
pattern := "^" + regexp.QuoteMeta(pn) + "$"
|
||||
pattern = strings.ReplaceAll(pattern, "\\*", ".*")
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
wildcards = append(wildcards, wildcardMapping{lotName: lot, re: re})
|
||||
continue
|
||||
}
|
||||
exact[pn] = append(exact[pn], lot)
|
||||
}
|
||||
for key := range exact {
|
||||
exact[key] = uniqueCaseInsensitive(exact[key])
|
||||
}
|
||||
|
||||
exactLot := make(map[string]string, len(lots))
|
||||
for _, l := range lots {
|
||||
name := strings.TrimSpace(l.LotName)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
exactLot[NormalizeKey(name)] = name
|
||||
}
|
||||
|
||||
return &MappingMatcher{
|
||||
exact: exact,
|
||||
exactLot: exactLot,
|
||||
wildcard: wildcards,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LotResolver) Resolve(partnumber string) (string, string, error) {
|
||||
key := NormalizeKey(partnumber)
|
||||
if key == "" {
|
||||
return "", "", ErrResolveNotFound
|
||||
}
|
||||
|
||||
if mapped := r.partnumberToLots[key]; len(mapped) > 0 {
|
||||
if len(mapped) == 1 {
|
||||
return mapped[0], "mapping_table", nil
|
||||
}
|
||||
return "", "", ErrResolveConflict
|
||||
}
|
||||
if exact, ok := r.exactLots[key]; ok {
|
||||
return exact, "article_exact", nil
|
||||
}
|
||||
|
||||
best := ""
|
||||
bestLen := -1
|
||||
tie := false
|
||||
for _, lot := range r.allLots {
|
||||
lotKey := NormalizeKey(lot)
|
||||
if lotKey == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(key, lotKey) {
|
||||
l := len([]rune(lotKey))
|
||||
if l > bestLen {
|
||||
best = lot
|
||||
bestLen = l
|
||||
tie = false
|
||||
} else if l == bestLen && !strings.EqualFold(best, lot) {
|
||||
tie = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if best == "" {
|
||||
return "", "", ErrResolveNotFound
|
||||
}
|
||||
if tie {
|
||||
return "", "", ErrResolveConflict
|
||||
}
|
||||
return best, "prefix", nil
|
||||
}
|
||||
|
||||
func (m *MappingMatcher) MatchLots(partnumber string) []string {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
key := NormalizeKey(partnumber)
|
||||
if key == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
lots := make([]string, 0, 2)
|
||||
if exact := m.exact[key]; len(exact) > 0 {
|
||||
lots = append(lots, exact...)
|
||||
}
|
||||
for _, wc := range m.wildcard {
|
||||
if wc.re == nil || !wc.re.MatchString(key) {
|
||||
continue
|
||||
}
|
||||
lots = append(lots, wc.lotName)
|
||||
}
|
||||
if lot, ok := m.exactLot[key]; ok && strings.TrimSpace(lot) != "" {
|
||||
lots = append(lots, lot)
|
||||
}
|
||||
return uniqueCaseInsensitive(lots)
|
||||
}
|
||||
|
||||
func NormalizeKey(v string) string {
|
||||
s := strings.ToLower(strings.TrimSpace(v))
|
||||
replacer := strings.NewReplacer(" ", "", "-", "", "_", "", ".", "", "/", "", "\\", "", "\"", "", "'", "", "(", "", ")", "")
|
||||
return replacer.Replace(s)
|
||||
}
|
||||
|
||||
func loadMappingsAndLots(db *gorm.DB) ([]models.LotPartnumber, []models.Lot, error) {
|
||||
var mappings []models.LotPartnumber
|
||||
if err := db.Find(&mappings).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var lots []models.Lot
|
||||
if err := db.Select("lot_name").Find(&lots).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return mappings, lots, nil
|
||||
}
|
||||
|
||||
func uniqueCaseInsensitive(values []string) []string {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
out := make([]string, 0, len(values))
|
||||
for _, v := range values {
|
||||
trimmed := strings.TrimSpace(v)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(trimmed)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, trimmed)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return strings.ToLower(out[i]) < strings.ToLower(out[j])
|
||||
})
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user