181 lines
5.6 KiB
Go
181 lines
5.6 KiB
Go
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
|
|
}
|