fix(shopee): normalize colors before AI matching #257
This commit is contained in:
@@ -131,6 +131,12 @@ func (service *Service) suggestMappings(ctx context.Context, id uint64, requestC
|
||||
item.Reason = "保留已确认映射"
|
||||
item.Apply = true
|
||||
response.MatchedCount++
|
||||
case role == RoleColor && deterministicColorMatch(value.Name, candidateNames) != "":
|
||||
item.PDDValue = deterministicColorMatch(value.Name, candidateNames)
|
||||
item.Status = "matched"
|
||||
item.Reason = "格式统一后唯一匹配"
|
||||
item.Apply = true
|
||||
response.MatchedCount++
|
||||
case role == RoleSize && deterministicSizeMatch(value.Name, candidateNames) != "":
|
||||
item.PDDValue = deterministicSizeMatch(value.Name, candidateNames)
|
||||
item.Status = "matched"
|
||||
@@ -237,6 +243,15 @@ func deterministicSizeMatch(value string, candidates []string) string {
|
||||
return match.MappedSize
|
||||
}
|
||||
|
||||
// Keep the original candidate label; normalization is only a comparison key.
|
||||
func deterministicColorMatch(value string, candidates []string) string {
|
||||
match, ok := aimatching.DeterministicMatch(aimatching.MatchRequest{TargetColor: value, Colors: candidates})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return match.MappedColor
|
||||
}
|
||||
|
||||
// aiSuggestErrorMessage turns an aimatching error into a message safe to
|
||||
// return to the admin UI (no credential, no raw provider body).
|
||||
func aiSuggestErrorMessage(err error) string {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package shopeeproduct
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go-admin/app/goauto/models"
|
||||
)
|
||||
|
||||
func TestColorNormalizedMatchWithoutAIKeepsOriginalLabels(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
pdd := seedPDDProduct(t, db, "active")
|
||||
pdd.SpecsJSON = `[{"name":"颜色","role":"color","values":[{"name":"260#大红","selectable":true},{"name":"260#墨绿","selectable":true},{"name":"AbC黑色","selectable":true}]}]`
|
||||
if err := db.Save(&pdd).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
service := NewService(db)
|
||||
sources := []string{"260#大紅", "260#墨綠", "abc 黑色"}
|
||||
targets := []string{"260#大红", "260#墨绿", "AbC黑色"}
|
||||
values := []SpecValue{}
|
||||
for _, name := range sources {
|
||||
values = append(values, SpecValue{Name: name, Source: ValueSourceImport})
|
||||
}
|
||||
created, err := service.Create(context.Background(), CreateRequest{RequestID: uuid.NewString(), ShopeeItemID: "normalized-color", PDDProductID: &pdd.ID, Specs: []SpecDimension{{Name: "颜色", Role: RoleColor, Values: values}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
preview, err := service.SuggestColorMappings(context.Background(), created.Product.ID, created.Product.SpecContextVersion)
|
||||
if err != nil {
|
||||
t.Fatalf("exact colors must not require AI: %v", err)
|
||||
}
|
||||
for i, item := range preview.Items {
|
||||
if item.Status != "matched" || !item.Apply || item.ValueName != sources[i] || item.PDDValue != targets[i] {
|
||||
t.Fatalf("unexpected preview: %+v", item)
|
||||
}
|
||||
}
|
||||
if len(preview.Items) != 3 || preview.MatchedCount != 3 {
|
||||
t.Fatal("missing exact matches")
|
||||
}
|
||||
var stored models.ShopeeProduct
|
||||
if err := db.First(&stored, created.Product.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
specs, err := Unmarshal(stored.SpecsJSON)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, value := range specs[0].Values {
|
||||
if value.Mapping != nil {
|
||||
t.Fatal("preview wrote mappings")
|
||||
}
|
||||
}
|
||||
result, err := service.AutoMatchMappings(context.Background(), created.Product.ID, AutoMatchRequest{RequestID: uuid.NewString(), SpecContextVersion: created.Product.SpecContextVersion})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.ConfirmedCount != 3 || result.UnmatchedCount != 0 {
|
||||
t.Fatalf("unexpected counts: %+v", result)
|
||||
}
|
||||
for i, value := range result.Product.Specs[0].Values {
|
||||
m := value.Mapping
|
||||
if value.Name != sources[i] || m == nil || m.PDDValue != targets[i] || m.Source != MappingSourceExactMatch || m.Status != MappingStatusConfirmed {
|
||||
t.Fatalf("incorrect persisted mapping: %+v", value)
|
||||
}
|
||||
}
|
||||
var after models.PDDProduct
|
||||
if err := db.First(&after, pdd.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if after.SpecsJSON != pdd.SpecsJSON {
|
||||
t.Fatal("changed original PDD specs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorNormalizedAmbiguityAndUnavailableStillRequireAI(t *testing.T) {
|
||||
for _, tc := range []struct{ name, source, specs string }{
|
||||
{"ambiguous", "大紅", `[{"name":"颜色","role":"color","values":[{"name":"大红","selectable":true},{"name":"大紅","selectable":true}]}]`},
|
||||
{"unavailable", "大紅", `[{"name":"颜色","role":"color","values":[{"name":"大红","selectable":false},{"name":"黑色","selectable":true}]}]`},
|
||||
{"unmatched", "深紅", `[{"name":"颜色","role":"color","values":[{"name":"大红","selectable":true}]}]`},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
pdd := seedPDDProduct(t, db, "active")
|
||||
pdd.SpecsJSON = tc.specs
|
||||
if err := db.Save(&pdd).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
service := NewService(db)
|
||||
created, err := service.Create(context.Background(), CreateRequest{RequestID: uuid.NewString(), ShopeeItemID: tc.name, PDDProductID: &pdd.ID, Specs: []SpecDimension{{Name: "颜色", Role: RoleColor, Values: []SpecValue{{Name: tc.source, Source: ValueSourceImport}}}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = service.SuggestColorMappings(context.Background(), created.Product.ID, created.Product.SpecContextVersion)
|
||||
if err == nil || errCode(t, err) != CodeAIUnavailable {
|
||||
t.Fatalf("must fall back to AI, not guess: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user