package service import ( "encoding/json" "fmt" "sort" "strings" "time" "cmautobuy/admin/repository" ) const ( SpecCompareUnlinked = "unlinked" SpecComparePDDMissing = "pdd_missing_or_deleted" SpecCompareShopeeUnavailable = "shopee_formal_specs_unavailable" SpecComparePDDUnavailable = "pdd_specs_unavailable" SpecCompareExact = "exact" SpecComparePDDMissingSpecs = "pdd_missing_specs" SpecComparePDDExtraSpecs = "pdd_extra_specs" SpecCompareDifferent = "different_specs" SpecCompareAmbiguousShared = "ambiguous_shared_pdd" ) // ShopSpecComparison 是 dry-run 报告中的单件商品结果,不包含标题、规格原文和价格。 type ShopSpecComparison struct { ShopeeGoodsID string `json:"shopee_goods_id"` PDDGoodsID string `json:"pdd_goods_id,omitempty"` Status string `json:"status"` ShopeeSpecCount int `json:"shopee_spec_count"` PDDSpecCount int `json:"pdd_spec_count"` MissingCount int `json:"missing_count"` ExtraCount int `json:"extra_count"` } // ShopSpecReport 是指定店铺的只读规格对比报告。 type ShopSpecReport struct { Shop string `json:"shop"` GeneratedAt string `json:"generated_at"` Total int `json:"total"` Summary map[string]int `json:"summary"` Products []ShopSpecComparison `json:"products"` } type pddComparableData struct { SKUs []struct { Options map[string]string `json:"options"` } `json:"skus"` } // CompareShopSpecs 只在内存中比较规格集合,不写数据库。 func CompareShopSpecs(shop string, sources []repository.ShopSpecSource, now time.Time) ShopSpecReport { report := ShopSpecReport{Shop: strings.TrimSpace(shop), GeneratedAt: now.UTC().Format(time.RFC3339), Summary: map[string]int{}} setsByShopee := make(map[string]map[string]struct{}, len(sources)) indexesByPDD := make(map[string][]int) for _, source := range sources { shopeeSet := formalShopeeSpecSet(source.ShopeeSpecs) setsByShopee[source.ShopeeGoodsID] = shopeeSet item := compareOne(source, shopeeSet) report.Products = append(report.Products, item) if source.PDDGoodsID != "" { indexesByPDD[source.PDDGoodsID] = append(indexesByPDD[source.PDDGoodsID], len(report.Products)-1) } } // 多个蝦皮商品共用同一个 PDD 时,只有正式规格集合完全相同才可作为后续回填候选。 for _, indexes := range indexesByPDD { if len(indexes) < 2 { continue } base := setsByShopee[report.Products[indexes[0]].ShopeeGoodsID] for _, index := range indexes[1:] { if !sameStringSet(base, setsByShopee[report.Products[index].ShopeeGoodsID]) { for _, ambiguousIndex := range indexes { report.Products[ambiguousIndex].Status = SpecCompareAmbiguousShared } break } } } sort.Slice(report.Products, func(i, j int) bool { return report.Products[i].ShopeeGoodsID < report.Products[j].ShopeeGoodsID }) report.Total = len(report.Products) for _, item := range report.Products { report.Summary[item.Status]++ } return report } func compareOne(source repository.ShopSpecSource, shopeeSet map[string]struct{}) ShopSpecComparison { item := ShopSpecComparison{ShopeeGoodsID: source.ShopeeGoodsID, PDDGoodsID: source.PDDGoodsID, ShopeeSpecCount: len(shopeeSet)} switch { case strings.TrimSpace(source.PDDGoodsID) == "": item.Status = SpecCompareUnlinked return item case !source.PDDExists || source.PDDDeleted: item.Status = SpecComparePDDMissing return item case len(shopeeSet) == 0: item.Status = SpecCompareShopeeUnavailable return item } pddSet, err := comparablePDDSpecSet(source.PDDSKUsJSON) if err != nil || len(pddSet) == 0 { item.Status = SpecComparePDDUnavailable return item } item.PDDSpecCount = len(pddSet) item.MissingCount = differenceCount(shopeeSet, pddSet) item.ExtraCount = differenceCount(pddSet, shopeeSet) switch { case item.MissingCount == 0 && item.ExtraCount == 0: item.Status = SpecCompareExact case item.MissingCount > 0 && item.ExtraCount == 0: item.Status = SpecComparePDDMissingSpecs case item.MissingCount == 0 && item.ExtraCount > 0: item.Status = SpecComparePDDExtraSpecs default: item.Status = SpecCompareDifferent } return item } func formalShopeeSpecSet(specs []repository.ComparableShopeeSpec) map[string]struct{} { set := make(map[string]struct{}) for _, spec := range specs { if !spec.ParseOK { continue } if key, ok := comparableSpecKey(spec.Color, spec.Size); ok { set[key] = struct{}{} } } return set } func comparablePDDSpecSet(raw string) (map[string]struct{}, error) { if strings.TrimSpace(raw) == "" { return nil, fmt.Errorf("PDD 规格为空") } var data pddComparableData if err := json.Unmarshal([]byte(raw), &data); err != nil { return nil, err } set := make(map[string]struct{}) for _, sku := range data.SKUs { for key := range sku.Options { if key != "color" && key != "size" { return nil, fmt.Errorf("存在无法比较的 PDD 规格维度 %q", key) } } if key, ok := comparableSpecKey(sku.Options["color"], sku.Options["size"]); ok { set[key] = struct{}{} } } return set, nil } func comparableSpecKey(color, size string) (string, bool) { color, size = strings.TrimSpace(color), strings.TrimSpace(size) if color == "" && size == "" { return "", false } encoded, _ := json.Marshal([]string{color, size}) return string(encoded), true } func differenceCount(left, right map[string]struct{}) int { count := 0 for value := range left { if _, exists := right[value]; !exists { count++ } } return count } func sameStringSet(left, right map[string]struct{}) bool { return len(left) == len(right) && differenceCount(left, right) == 0 }