package service import ( "database/sql" "encoding/json" "testing" "time" "cmautobuy/admin/repository" _ "modernc.org/sqlite" ) func TestBuildXLSXAvailabilityPlanConfirmsOnlySafeImportedSpecs(t *testing.T) { safe := `{"spec_source":"shopee_backfill","dimensions":[{"key":"color","name":"颜色"}],"skus":[` + `{"options":{"color":"黑"},"price_cent":2490,"list_price_cent":null,"inventory":7,"availability_status":"unknown"},` + `{"options":{"color":"白"},"price_cent":2490,"list_price_cent":3000,"availability_status":"unknown","available":null}]}` sources := XLSXPriceSources{PDDPrices: map[string]XLSXPDDPriceSource{ "GOOD": {}, "DECIDED": {}, "BAD_PRICE": {}, "WRONG_SOURCE": {}, "PENDING": {}, "MISSING": {}, }} targets := []repository.PDDPriceBackfillTarget{ {GoodsID: "GOOD", SKUsJSON: safe, CollectStatus: "collected", UpdatedAt: "old"}, {GoodsID: "DECIDED", SKUsJSON: `{"spec_source":"shopee_backfill","skus":[{"options":{"color":"黑"},"price_cent":100,"availability_status":"unknown","available":false}]}`, CollectStatus: "collected", UpdatedAt: "old"}, {GoodsID: "BAD_PRICE", SKUsJSON: `{"spec_source":"shopee_backfill","skus":[{"options":{"color":"黑"},"price_cent":0,"availability_status":"unknown"}]}`, CollectStatus: "collected", UpdatedAt: "old"}, {GoodsID: "WRONG_SOURCE", SKUsJSON: `{"spec_source":"client","skus":[{"options":{"color":"黑"},"price_cent":100,"availability_status":"unknown"}]}`, CollectStatus: "collected", UpdatedAt: "old"}, {GoodsID: "PENDING", SKUsJSON: safe, CollectStatus: "pending", UpdatedAt: "old"}, } plan := BuildXLSXAvailabilityPlan(sources, targets, time.Date(2026, 8, 14, 0, 0, 0, 0, time.UTC)) if len(plan.Candidates) != 1 || plan.Candidates[0].PDDGoodsID != "GOOD" || plan.Candidates[0].SKUCount != 2 { t.Fatalf("只应确认一个安全候选: %+v", plan) } if plan.Summary[XLSXAvailabilityEligible] != 1 || plan.Summary[XLSXAvailabilityAlreadyDecided] != 1 || plan.Summary[XLSXAvailabilityPriceInvalid] != 1 || plan.Summary[XLSXAvailabilitySourceMismatch] != 1 || plan.Summary[XLSXAvailabilityStatusUnsupported] != 1 || plan.Summary[XLSXAvailabilityPDDMissing] != 1 { t.Fatalf("跳过分类不正确: %+v", plan.Summary) } var root struct { SpecSource string `json:"spec_source"` SKUs []struct { Options map[string]string `json:"options"` PriceCent int64 `json:"price_cent"` ListPriceCent *int64 `json:"list_price_cent"` Inventory int `json:"inventory"` Available *bool `json:"available"` AvailabilityStatus *string `json:"availability_status"` } `json:"skus"` } if err := json.Unmarshal([]byte(plan.Candidates[0].NewSKUsJSON), &root); err != nil { t.Fatal(err) } if root.SpecSource != "shopee_backfill" || len(root.SKUs) != 2 { t.Fatalf("来源或 SKU 数被改坏: %+v", root) } for _, sku := range root.SKUs { if sku.Available == nil || !*sku.Available || sku.AvailabilityStatus != nil || sku.PriceCent != 2490 { t.Fatalf("应只确认 available 并删除 unknown 标记: %+v", sku) } } if root.SKUs[0].Inventory != 7 || root.SKUs[1].ListPriceCent == nil || *root.SKUs[1].ListPriceCent != 3000 { t.Fatalf("库存和列表价必须保留: %+v", root.SKUs) } } func TestConfirmBackfilledPDDAvailabilityRejectsPartialOrAmbiguousRows(t *testing.T) { tests := []struct { name, raw, want string }{ {"空规格", `{"spec_source":"shopee_backfill","skus":[{"options":{},"price_cent":100,"availability_status":"unknown"}]}`, XLSXAvailabilitySpecsInvalid}, {"缺价格", `{"spec_source":"shopee_backfill","skus":[{"options":{"color":"黑"},"availability_status":"unknown"}]}`, XLSXAvailabilityPriceInvalid}, {"缺未知标记", `{"spec_source":"shopee_backfill","skus":[{"options":{"color":"黑"},"price_cent":100}]}`, XLSXAvailabilityMarkerInvalid}, {"已有可售", `{"spec_source":"shopee_backfill","skus":[{"options":{"color":"黑"},"price_cent":100,"availability_status":"unknown","available":true}]}`, XLSXAvailabilityAlreadyDecided}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if _, _, got := confirmBackfilledPDDAvailability(test.raw); got != test.want { t.Fatalf("状态=%q,期望 %q", got, test.want) } }) } } func TestApplyXLSXAvailabilityPlanUsesWholeBatchRollback(t *testing.T) { db, err := sql.Open("sqlite", ":memory:") if err != nil { t.Fatal(err) } defer db.Close() db.SetMaxOpenConns(1) if _, err := db.Exec(`CREATE TABLE pdd_products( goods_id TEXT PRIMARY KEY, skus_json TEXT, collect_status TEXT, deleted_at TEXT, updated_at TEXT)`); err != nil { t.Fatal(err) } if _, err := db.Exec(`INSERT INTO pdd_products(goods_id,skus_json,collect_status,updated_at) VALUES('P1','old-1','collected','t1'),('P2','changed','collected','t2')`); err != nil { t.Fatal(err) } plan := XLSXAvailabilityPlan{Candidates: []XLSXAvailabilityCandidate{ {PDDGoodsID: "P1", NewSKUsJSON: `{"skus":[1]}`, Original: repository.PDDPriceBackfillTarget{GoodsID: "P1", SKUsJSON: "old-1", CollectStatus: "collected", UpdatedAt: "t1"}}, {PDDGoodsID: "P2", NewSKUsJSON: `{"skus":[2]}`, Original: repository.PDDPriceBackfillTarget{GoodsID: "P2", SKUsJSON: "old-2", CollectStatus: "collected", UpdatedAt: "t2"}}, }} if _, err := ApplyXLSXAvailabilityPlan(db, plan, time.Now()); err == nil { t.Fatal("第二件原值变化时必须失败") } var raw, status, updatedAt string if err := db.QueryRow(`SELECT skus_json,collect_status,updated_at FROM pdd_products WHERE goods_id='P1'`).Scan(&raw, &status, &updatedAt); err != nil { t.Fatal(err) } if raw != "old-1" || status != "collected" || updatedAt != "t1" { t.Fatalf("整批必须回滚,实际 raw=%q status=%q updated_at=%q", raw, status, updatedAt) } }