package service import ( "encoding/json" "strings" "testing" "time" "cmautobuy/admin/model" "cmautobuy/admin/repository" ) func TestUpdateShopeeSKUManual_保存并保留身份字段(t *testing.T) { db := newCatalogTestDB(t) actor := &model.User{UserID: "USER-1", Role: model.RolePurchaser, Status: model.UserActive} goodsID, recordID := "S-EDIT", "REC-EDIT" nowText := "2026-08-12T00:00:00Z" if _, err := db.Exec(`INSERT INTO shopee_products(goods_id,title,source,created_at,updated_at) VALUES(?,?,'api',?,?)`, goodsID, "测试商品", nowText, nowText); err != nil { t.Fatal(err) } if _, err := db.Exec(`INSERT INTO shopee_skus(sku_id,shopee_sku_id,goods_id,spec_raw,spec_key,color,size,advice,parse_ok,sku_code,is_manual,source,field_sources,field_observed_at,source_observed_at,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, recordID, "REAL-SKU-1", goodsID, "旧黑色,M【建议50公斤】", "稳定规格键", "旧黑色", "M", "旧建议", 0, "CODE-1", 0, "partner", `{"color":"partner","size":"partner","advice":"partner","sku_code":"partner"}`, `{"color":"2026-08-12T00:00:00Z","size":"2026-08-12T00:00:00Z","advice":"2026-08-12T00:00:00Z","sku_code":"2026-08-12T00:00:00Z"}`, nowText, nowText, nowText); err != nil { t.Fatal(err) } updatedAt := time.Date(2026, 8, 13, 3, 4, 5, 0, time.UTC) if err := UpdateShopeeSKUManual(db, actor, goodsID, recordID, " 新黑色 ", " L ", "", updatedAt); err != nil { t.Fatal(err) } var shopeeSKU, specRaw, specKey, color, size, advice, skuCode, source, sourcesRaw, timesRaw, storedAt string var parseOK, isManual int if err := db.QueryRow(`SELECT shopee_sku_id,spec_raw,spec_key,color,size,COALESCE(advice,''),parse_ok,sku_code,is_manual,source,field_sources,field_observed_at,updated_at FROM shopee_skus WHERE sku_id=?`, recordID). Scan(&shopeeSKU, &specRaw, &specKey, &color, &size, &advice, &parseOK, &skuCode, &isManual, &source, &sourcesRaw, ×Raw, &storedAt); err != nil { t.Fatal(err) } if color != "新黑色" || size != "L" || advice != "" || parseOK != 1 { t.Fatalf("人工字段未正确保存:color=%q size=%q advice=%q parse=%d", color, size, advice, parseOK) } if shopeeSKU != "REAL-SKU-1" || specRaw != "旧黑色,M【建议50公斤】" || specKey != "稳定规格键" || skuCode != "CODE-1" || isManual != 0 || source != "partner" { t.Fatalf("不可变身份被修改:sku=%q raw=%q key=%q code=%q manual=%d source=%q", shopeeSKU, specRaw, specKey, skuCode, isManual, source) } var sources, times map[string]string if err := json.Unmarshal([]byte(sourcesRaw), &sources); err != nil { t.Fatal(err) } if err := json.Unmarshal([]byte(timesRaw), ×); err != nil { t.Fatal(err) } for _, field := range []string{"color", "size", "advice"} { if sources[field] != "manual" || times[field] != updatedAt.Format(model.TimeLayout) { t.Fatalf("%s 的人工来源错误:sources=%v times=%v", field, sources, times) } } if sources["sku_code"] != "partner" || storedAt != updatedAt.Format(model.TimeLayout) { t.Fatalf("未编辑字段来源或更新时间错误:sources=%v updated=%q", sources, storedAt) } for _, policy := range []string{"fill_missing", "overwrite_same_source"} { outcome, err := repository.UpsertCatalogShopeeSKU(db, repository.CatalogShopeeSKUInput{ RecordID: "IGNORED", ShopeeSKUID: "REAL-SKU-1", GoodsID: goodsID, SpecRaw: specRaw, SpecKey: specKey, Color: "导入黑色", Size: "XL", Advice: "导入重新填入的建议", ParseOK: true, SKUCode: "CODE-2", Source: "partner", ObservedAt: "2026-08-14T00:00:00Z", Now: "2026-08-14T00:00:00Z", UpdatePolicy: policy, }) if err != nil { t.Fatalf("%s 导入失败:%v", policy, err) } if outcome != repository.CatalogSKUFilled && outcome != repository.CatalogSKUSkipped && outcome != repository.CatalogSKUSameSourceUpdated { t.Fatalf("%s 返回意外结果:%s", policy, outcome) } } if err := db.QueryRow(`SELECT color,size,COALESCE(advice,'') FROM shopee_skus WHERE sku_id=?`, recordID).Scan(&color, &size, &advice); err != nil { t.Fatal(err) } if color != "新黑色" || size != "L" || advice != "" { t.Fatalf("人工字段被后续导入覆盖:%q/%q/%q", color, size, advice) } } func TestUpdateShopeeSKUManual_拒绝无效输入且不改数据(t *testing.T) { cases := []struct { name string actor *model.User goodsID, recordID string color, size string }{ {"未登录", nil, "S-EDIT", "REC-EDIT", "黑色", "M"}, {"停用账号", &model.User{Status: model.UserDisabled}, "S-EDIT", "REC-EDIT", "黑色", "M"}, {"规格不存在", &model.User{Status: model.UserActive}, "S-EDIT", "NOT-FOUND", "黑色", "M"}, {"跨商品", &model.User{Status: model.UserActive}, "S-OTHER", "REC-EDIT", "黑色", "M"}, {"颜色为空", &model.User{Status: model.UserActive}, "S-EDIT", "REC-EDIT", " ", "M"}, {"尺码为空", &model.User{Status: model.UserActive}, "S-EDIT", "REC-EDIT", "黑色", " "}, {"颜色过长", &model.User{Status: model.UserActive}, "S-EDIT", "REC-EDIT", strings.Repeat("色", 501), "M"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { db := newCatalogTestDB(t) now := "2026-08-12T00:00:00Z" if _, err := db.Exec(`INSERT INTO shopee_products(goods_id,title,source,created_at,updated_at) VALUES('S-EDIT','测试','api',?,?)`, now, now); err != nil { t.Fatal(err) } if _, err := db.Exec(`INSERT INTO shopee_skus(sku_id,goods_id,spec_raw,spec_key,color,size,parse_ok,is_manual,source,field_sources,field_observed_at,created_at,updated_at) VALUES('REC-EDIT','S-EDIT','黑色,M','黑色,M','旧色','旧码',1,0,'partner','{}','{}',?,?)`, now, now); err != nil { t.Fatal(err) } err := UpdateShopeeSKUManual(db, tc.actor, tc.goodsID, tc.recordID, tc.color, tc.size, "建议", time.Now()) if err == nil { t.Fatal("期望拒绝无效输入") } var color, size string if err := db.QueryRow(`SELECT color,size FROM shopee_skus WHERE sku_id='REC-EDIT'`).Scan(&color, &size); err != nil { t.Fatal(err) } if color != "旧色" || size != "旧码" { t.Fatalf("失败后发生部分更新:%q/%q", color, size) } }) } }