2026-08-11 10:06:10 +08:00
package service
import (
"database/sql"
"encoding/json"
"errors"
"testing"
_ "modernc.org/sqlite"
"cmautobuy/admin/repository"
)
func newCatalogTestDB ( t * testing . T ) * sql . DB {
t . Helper ()
db , err := sql . Open ( "sqlite" , ":memory:" )
if err != nil {
t . Fatal ( err )
}
db . SetMaxOpenConns ( 1 )
statements := [] string {
2026-08-13 11:15:54 +08:00
`CREATE TABLE shops(shop_id TEXT PRIMARY KEY,display_name TEXT,normalized_name TEXT,enabled INTEGER,created_by_user_id TEXT,created_at TEXT,updated_at TEXT)` ,
`CREATE TABLE shop_channel_aliases(alias_id TEXT PRIMARY KEY,shop_id TEXT,channel TEXT,alias_name TEXT,normalized_alias TEXT,enabled INTEGER,created_at TEXT,updated_at TEXT)` ,
`CREATE TABLE shopee_products(goods_id TEXT PRIMARY KEY,shop_id TEXT,title TEXT NOT NULL,shopee_status TEXT,main_sku_code TEXT,image_url TEXT,shopee_shop_name TEXT,image_source TEXT,image_observed_at TEXT,image_is_manual INTEGER DEFAULT 0,shop_name_source TEXT,shop_name_observed_at TEXT,shop_name_is_manual INTEGER DEFAULT 0,source TEXT,source_observed_at TEXT,pdd_goods_url TEXT,pdd_goods_id TEXT,deleted_at TEXT,deleted_by_user_id TEXT,created_at TEXT,updated_at TEXT)` ,
2026-08-11 11:03:18 +08:00
`CREATE TABLE shopee_skus(sku_id TEXT PRIMARY KEY,shopee_sku_id TEXT UNIQUE,goods_id TEXT NOT NULL,spec_raw TEXT,spec_key TEXT,color TEXT,size TEXT,advice TEXT,parse_ok INTEGER,sku_code TEXT,is_manual INTEGER,source TEXT,field_sources TEXT,field_observed_at TEXT,source_observed_at TEXT,created_at TEXT,updated_at TEXT,UNIQUE(goods_id,spec_key))` ,
2026-08-11 10:06:10 +08:00
`CREATE TABLE pdd_products(id INTEGER PRIMARY KEY AUTOINCREMENT,goods_id TEXT UNIQUE,url TEXT,title TEXT,shop_name TEXT,skus_json TEXT,collect_status TEXT,collect_msg TEXT,artifact_ref TEXT,collected_at TEXT,deleted_at TEXT,source TEXT,source_observed_at TEXT,created_at TEXT,updated_at TEXT)` ,
2026-08-11 11:18:51 +08:00
`CREATE TABLE catalog_import_runs(source TEXT,batch_id TEXT,request_hash TEXT,status TEXT,request_count INTEGER,conflict_count INTEGER,observed_at TEXT,update_policy TEXT DEFAULT 'fill_missing',last_request_at TEXT,last_conflict_at TEXT,shopee_created INTEGER DEFAULT 0,shopee_updated INTEGER DEFAULT 0,shopee_fields_filled INTEGER DEFAULT 0,shopee_fields_same_source_updated INTEGER DEFAULT 0,shopee_fields_manual_skipped INTEGER DEFAULT 0,shopee_fields_stale_skipped INTEGER DEFAULT 0,sku_created INTEGER DEFAULT 0,sku_updated INTEGER DEFAULT 0,sku_filled INTEGER DEFAULT 0,sku_same_source_updated INTEGER DEFAULT 0,sku_skipped INTEGER DEFAULT 0,sku_manual_skipped INTEGER DEFAULT 0,sku_stale_skipped INTEGER DEFAULT 0,pdd_created INTEGER DEFAULT 0,pdd_updated INTEGER DEFAULT 0,association_created INTEGER DEFAULT 0,association_unchanged INTEGER DEFAULT 0,failure_count INTEGER DEFAULT 0,error_summary TEXT,response_body TEXT,created_at TEXT,finished_at TEXT,PRIMARY KEY(source,batch_id))` ,
2026-08-11 10:06:10 +08:00
}
for _ , statement := range statements {
if _ , err := db . Exec ( statement ); err != nil {
t . Fatal ( err )
}
}
t . Cleanup ( func () { db . Close () })
return db
}
func validCatalogBatch () CatalogBatchRequest {
price := int64 ( 1299 )
return CatalogBatchRequest { SchemaVersion : 1 , BatchID : "batch-001" , ObservedAt : "2026-08-11T08:00:00+08:00" ,
ShopeeProducts : [] CatalogShopeeProduct {{ GoodsID : "S-1" , Title : "蝦皮上衣" }},
ShopeeSKUs : [] CatalogShopeeSKU {{ SKUID : "SKU-1" , GoodsID : "S-1" , SpecRaw : "黑色,M" , Color : "黑色" , Size : "M" , ParseOK : true }},
PddProducts : [] CatalogPddProduct {{ GoodsID : "P-1" , URL : "https://mobile.yangkeduo.com/goods.html?goods_id=P-1" , Title : "PDD上衣" , Dimensions : [] CatalogPddDimension {{ Key : "color" , Name : "颜色" }}, SKUs : [] CatalogPddSKU {{ Options : map [ string ] string { "color" : "黑色" }, PriceCent : & price , Available : true }}}},
Associations : [] CatalogAssociation {{ ShopeeGoodsID : "S-1" , PddGoodsID : "P-1" }}}
}
func TestImportCatalogBatch_原子写入重放与冲突 ( t * testing . T ) {
db := newCatalogTestDB ( t )
req := validCatalogBatch ()
raw , _ := json . Marshal ( req )
got , err := ImportCatalogBatch ( db , "script-a" , req , raw )
if err != nil {
t . Fatal ( err )
}
if got . Counts . ShopeeCreated != 1 || got . Counts . SKUCreated != 1 || got . Counts . PddCreated != 1 || got . Counts . AssociationCreated != 1 {
t . Fatalf ( "统计不正确:%+v" , got )
}
replay , err := ImportCatalogBatch ( db , "script-a" , req , raw )
if err != nil || ! replay . Replayed {
t . Fatalf ( "相同批次应重放:resp=%+v err=%v" , replay , err )
}
changed := append ([] byte {}, raw ... )
changed = append ( changed , ' ' )
_ , err = ImportCatalogBatch ( db , "script-a" , req , changed )
var catalogErr * CatalogError
if ! errors . As ( err , & catalogErr ) || catalogErr . Code != "IDEMPOTENCY_CONFLICT" {
t . Fatalf ( "不同内容应冲突:%v" , err )
}
run , err := repository . GetCatalogImportRun ( db , "script-a" , req . BatchID )
if err != nil || run . RequestCount != 3 || run . ConflictCount != 1 {
t . Fatalf ( "批次计数不正确:%+v err=%v" , run , err )
}
var pddID string
if err := db . QueryRow ( `SELECT pdd_goods_id FROM shopee_products WHERE goods_id='S-1'` ). Scan ( & pddID ); err != nil || pddID != "P-1" {
t . Fatalf ( "关联未写入:%q %v" , pddID , err )
}
}
func TestImportCatalogBatch_关联冲突整批回滚且同请求重放错误 ( t * testing . T ) {
db := newCatalogTestDB ( t )
now := "2026-08-11T00:00:00.000000000Z"
_ , _ = db . Exec ( `INSERT INTO shopee_products(goods_id,title,source,pdd_goods_id,created_at,updated_at) VALUES('S-1','旧标题','report','P-OLD',?,?)` , now , now )
_ , _ = db . Exec ( `INSERT INTO pdd_products(goods_id,url,collect_status,created_at,updated_at) VALUES('P-OLD','old','pending',?,?)` , now , now )
req := validCatalogBatch ()
raw , _ := json . Marshal ( req )
_ , err := ImportCatalogBatch ( db , "script-a" , req , raw )
var first * CatalogError
if ! errors . As ( err , & first ) || first . Code != "ASSOCIATION_CONFLICT" {
t . Fatalf ( "应返回关联冲突:%v" , err )
}
var title string
_ = db . QueryRow ( `SELECT title FROM shopee_products WHERE goods_id='S-1'` ). Scan ( & title )
if title != "旧标题" {
t . Fatalf ( "失败批次必须整体回滚,title=%q" , title )
}
_ , err = ImportCatalogBatch ( db , "script-a" , req , raw )
var replay * CatalogError
if ! errors . As ( err , & replay ) || replay . Code != first . Code || replay . Status != first . Status {
t . Fatalf ( "失败重放应保持原错误:%+v" , err )
}
}
func TestValidateCatalogBatch_拒绝旧版超限和负金额 ( t * testing . T ) {
req := validCatalogBatch ()
req . SchemaVersion = 2
if err := ValidateCatalogBatch ( req ); err == nil {
t . Fatal ( "应拒绝未知 schema" )
}
req = validCatalogBatch ()
negative := int64 ( - 1 )
req . PddProducts [ 0 ]. SKUs [ 0 ]. PriceCent = & negative
if err := ValidateCatalogBatch ( req ); err == nil {
t . Fatal ( "应拒绝负金额" )
}
2026-08-11 11:03:18 +08:00
req = validCatalogBatch ()
req . UpdatePolicy = "force_overwrite"
if err := ValidateCatalogBatch ( req ); err == nil {
t . Fatal ( "应拒绝强制覆盖策略" )
}
}
func TestImportCatalogBatch_无真实SKU并在后续补回同一记录 ( t * testing . T ) {
db := newCatalogTestDB ( t )
req := validCatalogBatch ()
req . BatchID = "missing-id"
req . ShopeeSKUs [ 0 ]. SKUID = ""
raw , _ := json . Marshal ( req )
if got , err := ImportCatalogBatch ( db , "script-a" , req , raw ); err != nil || got . Counts . SKUCreated != 1 {
t . Fatalf ( "首次导入失败:%+v %v" , got , err )
}
var recordID string
if err := db . QueryRow ( `SELECT sku_id FROM shopee_skus WHERE goods_id='S-1'` ). Scan ( & recordID ); err != nil || recordID == "" {
t . Fatalf ( "内部记录缺失:%q %v" , recordID , err )
}
req . BatchID = "fill-real-id"
req . ShopeeSKUs [ 0 ]. SKUID = "REAL-1"
raw , _ = json . Marshal ( req )
got , err := ImportCatalogBatch ( db , "script-a" , req , raw )
if err != nil || got . Counts . SKUFilled != 1 {
t . Fatalf ( "补真实 ID 失败:%+v %v" , got , err )
}
var after , external string
_ = db . QueryRow ( `SELECT sku_id,shopee_sku_id FROM shopee_skus WHERE goods_id='S-1'` ). Scan ( & after , & external )
if after != recordID || external != "REAL-1" {
t . Fatalf ( "不应新建记录:before=%q after=%q external=%q" , recordID , after , external )
}
}
func TestImportCatalogBatch_三种策略和保护边界 ( t * testing . T ) {
db := newCatalogTestDB ( t )
req := validCatalogBatch ()
req . ShopeeSKUs [ 0 ]. Color = ""
raw , _ := json . Marshal ( req )
if _ , err := ImportCatalogBatch ( db , "script-a" , req , raw ); err != nil {
t . Fatal ( err )
}
fill := validCatalogBatch ()
fill . BatchID = "fill"
fill . ShopeeSKUs [ 0 ]. Color = "黑色"
fill . ShopeeSKUs [ 0 ]. Size = "L"
raw , _ = json . Marshal ( fill )
got , err := ImportCatalogBatch ( db , "script-a" , fill , raw )
if err != nil || got . Counts . SKUFilled != 1 {
t . Fatalf ( "补空失败:%+v %v" , got , err )
}
var color , size string
_ = db . QueryRow ( `SELECT color,size FROM shopee_skus` ). Scan ( & color , & size )
if color != "黑色" || size != "M" {
t . Fatalf ( "fill_missing 覆盖了非空字段:%q/%q" , color , size )
}
insertOnly := validCatalogBatch ()
insertOnly . BatchID = "insert"
insertOnly . UpdatePolicy = "insert_only"
insertOnly . ShopeeSKUs [ 0 ]. Color = "白色"
raw , _ = json . Marshal ( insertOnly )
got , err = ImportCatalogBatch ( db , "script-a" , insertOnly , raw )
if err != nil || got . Counts . SKUSkipped != 1 {
t . Fatalf ( "insert_only 失败:%+v %v" , got , err )
}
overwrite := validCatalogBatch ()
overwrite . BatchID = "overwrite"
overwrite . UpdatePolicy = "overwrite_same_source"
overwrite . ObservedAt = "2026-08-12T08:00:00+08:00"
overwrite . ShopeeSKUs [ 0 ]. Color = "白色"
raw , _ = json . Marshal ( overwrite )
got , err = ImportCatalogBatch ( db , "script-a" , overwrite , raw )
if err != nil || got . Counts . SKUSameSourceUpdated != 1 {
t . Fatalf ( "同来源覆盖失败:%+v %v" , got , err )
}
_ = db . QueryRow ( `SELECT color FROM shopee_skus` ). Scan ( & color )
if color != "白色" {
t . Fatalf ( "color=%q" , color )
}
stale := validCatalogBatch ()
stale . BatchID = "stale"
stale . UpdatePolicy = "overwrite_same_source"
stale . ObservedAt = "2026-08-11T12:00:00+08:00"
stale . ShopeeSKUs [ 0 ]. Color = "旧红色"
raw , _ = json . Marshal ( stale )
got , err = ImportCatalogBatch ( db , "script-a" , stale , raw )
if err != nil || got . Counts . SKUStaleSkipped != 1 {
t . Fatalf ( "旧观测保护失败:%+v %v" , got , err )
}
cross := validCatalogBatch ()
cross . BatchID = "cross-source"
cross . ObservedAt = "2026-08-13T08:00:00+08:00"
cross . ShopeeSKUs [ 0 ]. Advice = "来源B建议"
raw , _ = json . Marshal ( cross )
if _ , err := ImportCatalogBatch ( db , "script-b" , cross , raw ); err != nil {
t . Fatal ( err )
}
protect := validCatalogBatch ()
protect . BatchID = "protect-cross"
protect . UpdatePolicy = "overwrite_same_source"
protect . ObservedAt = "2026-08-14T08:00:00+08:00"
protect . ShopeeSKUs [ 0 ]. Advice = "来源A不得覆盖"
raw , _ = json . Marshal ( protect )
if _ , err := ImportCatalogBatch ( db , "script-a" , protect , raw ); err != nil {
t . Fatal ( err )
}
var advice string
_ = db . QueryRow ( `SELECT advice FROM shopee_skus` ). Scan ( & advice )
if advice != "来源B建议" {
t . Fatalf ( "跨来源字段被覆盖:%q" , advice )
}
_ , _ = db . Exec ( `UPDATE shopee_skus SET is_manual=1` )
manual := validCatalogBatch ()
manual . BatchID = "manual"
manual . ObservedAt = "2026-08-13T08:00:00+08:00"
manual . ShopeeSKUs [ 0 ]. Color = "红色"
raw , _ = json . Marshal ( manual )
got , err = ImportCatalogBatch ( db , "script-a" , manual , raw )
if err != nil || got . Counts . SKUManualSkipped != 1 {
t . Fatalf ( "人工保护失败:%+v %v" , got , err )
}
}
func TestImportCatalogBatch_同规格不同真实SKU冲突并回滚 ( t * testing . T ) {
db := newCatalogTestDB ( t )
req := validCatalogBatch ()
raw , _ := json . Marshal ( req )
if _ , err := ImportCatalogBatch ( db , "script-a" , req , raw ); err != nil {
t . Fatal ( err )
}
req . BatchID = "conflict"
req . ShopeeSKUs [ 0 ]. SKUID = "SKU-2"
req . ShopeeProducts [ 0 ]. Title = "不应提交"
raw , _ = json . Marshal ( req )
_ , err := ImportCatalogBatch ( db , "script-a" , req , raw )
var catalogErr * CatalogError
if ! errors . As ( err , & catalogErr ) || catalogErr . Status != 409 {
t . Fatalf ( "应返回 409: %v" , err )
}
var title string
_ = db . QueryRow ( `SELECT title FROM shopee_products WHERE goods_id='S-1'` ). Scan ( & title )
if title != "蝦皮上衣" {
t . Fatalf ( "冲突批次未整体回滚:%q" , title )
}
2026-08-11 10:06:10 +08:00
}
2026-08-11 11:18:51 +08:00
func TestImportCatalogBatch_蝦皮图片店铺分级更新和人工保护 ( t * testing . T ) {
db := newCatalogTestDB ( t )
base := validCatalogBatch ()
raw , _ := json . Marshal ( base )
if _ , err := ImportCatalogBatch ( db , "script-a" , base , raw ); err != nil {
t . Fatal ( err )
}
fill := validCatalogBatch ()
fill . BatchID = "product-fill"
fill . ShopeeProducts [ 0 ]. ImageURL = "https://img.example.com/a.jpg"
fill . ShopeeProducts [ 0 ]. ShopName = "蝦皮店铺A"
raw , _ = json . Marshal ( fill )
got , err := ImportCatalogBatch ( db , "script-a" , fill , raw )
if err != nil || got . Counts . ShopeeFieldsFilled != 2 {
t . Fatalf ( "补空失败:%+v %v" , got , err )
}
keep := validCatalogBatch ()
keep . BatchID = "product-keep"
keep . ShopeeProducts [ 0 ]. ImageURL = "https://img.example.com/b.jpg"
keep . ShopeeProducts [ 0 ]. ShopName = "店铺B"
raw , _ = json . Marshal ( keep )
got , err = ImportCatalogBatch ( db , "script-b" , keep , raw )
if err != nil || got . Counts . ShopeeFieldsFilled != 0 {
t . Fatalf ( "默认策略不应覆盖:%+v %v" , got , err )
}
cross := keep
cross . BatchID = "product-cross"
cross . UpdatePolicy = "overwrite_same_source"
cross . ObservedAt = "2026-08-12T07:00:00+08:00"
raw , _ = json . Marshal ( cross )
got , err = ImportCatalogBatch ( db , "script-b" , cross , raw )
if err != nil || got . Counts . ShopeeFieldsSameSourceUpdated != 0 {
t . Fatalf ( "跨来源不应覆盖:%+v %v" , got , err )
}
insertOnly := keep
insertOnly . BatchID = "product-insert-only"
insertOnly . UpdatePolicy = "insert_only"
raw , _ = json . Marshal ( insertOnly )
got , err = ImportCatalogBatch ( db , "script-a" , insertOnly , raw )
if err != nil || got . Counts . ShopeeFieldsFilled != 0 || got . Counts . ShopeeFieldsSameSourceUpdated != 0 {
t . Fatalf ( "insert_only 不应修改已有商品:%+v %v" , got , err )
}
overwrite := keep
overwrite . BatchID = "product-overwrite"
overwrite . UpdatePolicy = "overwrite_same_source"
overwrite . ObservedAt = "2026-08-12T08:00:00+08:00"
raw , _ = json . Marshal ( overwrite )
got , err = ImportCatalogBatch ( db , "script-a" , overwrite , raw )
if err != nil || got . Counts . ShopeeFieldsSameSourceUpdated != 2 {
t . Fatalf ( "同来源更新失败:%+v %v" , got , err )
}
var image , shop string
_ = db . QueryRow ( `SELECT image_url,shopee_shop_name FROM shopee_products WHERE goods_id='S-1'` ). Scan ( & image , & shop )
if image != "https://img.example.com/b.jpg" || shop != "店铺B" {
t . Fatalf ( "字段未更新:%q %q" , image , shop )
}
stale := overwrite
stale . BatchID = "product-stale"
stale . ObservedAt = "2026-08-11T09:00:00+08:00"
stale . ShopeeProducts [ 0 ]. ImageURL = "https://img.example.com/old.jpg"
stale . ShopeeProducts [ 0 ]. ShopName = "旧店铺"
raw , _ = json . Marshal ( stale )
got , err = ImportCatalogBatch ( db , "script-a" , stale , raw )
if err != nil || got . Counts . ShopeeFieldsStaleSkipped != 2 {
t . Fatalf ( "旧观测保护失败:%+v %v" , got , err )
}
_ , _ = db . Exec ( `UPDATE shopee_products SET image_is_manual=1,shop_name_is_manual=1 WHERE goods_id='S-1'` )
manual := overwrite
manual . BatchID = "product-manual"
manual . ObservedAt = "2026-08-13T08:00:00+08:00"
manual . ShopeeProducts [ 0 ]. ImageURL = "https://img.example.com/c.jpg"
manual . ShopeeProducts [ 0 ]. ShopName = "店铺C"
raw , _ = json . Marshal ( manual )
got , err = ImportCatalogBatch ( db , "script-a" , manual , raw )
if err != nil || got . Counts . ShopeeFieldsManualSkipped != 2 {
t . Fatalf ( "人工保护失败:%+v %v" , got , err )
}
}
func TestValidateCatalogBatch_拒绝危险图片URL ( t * testing . T ) {
for _ , rawURL := range [] string { "file:///tmp/a.jpg" , "data:image/png;base64,AA" , "https://user:pass@example.com/a.jpg" , "https://example.com/a.jpg?access_token=secret" , "javascript:alert(1)" } {
req := validCatalogBatch ()
req . ShopeeProducts [ 0 ]. ImageURL = rawURL
if err := ValidateCatalogBatch ( req ); err == nil {
t . Errorf ( "应拒绝 %q" , rawURL )
}
}
req := validCatalogBatch ()
req . ShopeeProducts [ 0 ]. ImageURL = "https://img.example.com/a.jpg?v=1"
if err := ValidateCatalogBatch ( req ); err != nil {
t . Fatalf ( "合法图片被拒绝:%v" , err )
}
}
2026-08-20 14:41:01 +08:00
func TestPreviewCatalogBatch_只读且报告既有关联冲突 ( t * testing . T ) {
db := newCatalogTestDB ( t )
base := validCatalogBatch ()
raw , _ := json . Marshal ( base )
if _ , err := ImportCatalogBatch ( db , "script-a" , base , raw ); err != nil {
t . Fatal ( err )
}
preview := validCatalogBatch ()
preview . BatchID = "preview-only"
preview . DryRun = true
preview . Associations [ 0 ]. PddGoodsID = "P-2"
preview . PddProducts [ 0 ]. GoodsID = "P-2"
preview . PddProducts [ 0 ]. URL = "https://mobile.yangkeduo.com/goods.html?goods_id=P-2"
got , err := ImportCatalogBatch ( db , "script-a" , preview , [] byte ( "unused" ))
if err != nil || got . Status != "previewed" || len ( got . Conflicts ) != 1 {
t . Fatalf ( "预检结果错误:%+v %v" , got , err )
}
var runs , pdd int
_ = db . QueryRow ( `SELECT COUNT(*) FROM catalog_import_runs WHERE batch_id='preview-only'` ). Scan ( & runs )
_ = db . QueryRow ( `SELECT COUNT(*) FROM pdd_products WHERE goods_id='P-2'` ). Scan ( & pdd )
if runs != 0 || pdd != 0 {
t . Fatalf ( "预检写入了数据:runs=%d pdd=%d" , runs , pdd )
}
}
func TestImportCatalogBatch_FillMissing不覆盖已有PDD和商品字段 ( t * testing . T ) {
db := newCatalogTestDB ( t )
base := validCatalogBatch ()
raw , _ := json . Marshal ( base )
if _ , err := ImportCatalogBatch ( db , "script-a" , base , raw ); err != nil { t . Fatal ( err ) }
_ , _ = db . Exec ( `UPDATE shopee_products SET title='人工标题',main_sku_code='人工货号' WHERE goods_id='S-1'` )
_ , _ = db . Exec ( `UPDATE pdd_products SET url='https://manual.example/P-1',title='已采集标题',shop_name='已采集店铺',collect_status='collected' WHERE goods_id='P-1'` )
fill := validCatalogBatch (); fill . BatchID = "no-overwrite" ; fill . ShopeeProducts [ 0 ]. Title = "来源标题" ; fill . ShopeeProducts [ 0 ]. MainSKUCode = "来源货号" ; fill . PddProducts [ 0 ]. URL = "https://mobile.yangkeduo.com/goods.html?goods_id=P-1" ; fill . PddProducts [ 0 ]. Title = "来源PDD标题" ; fill . PddProducts [ 0 ]. ShopName = "来源PDD店铺"
raw , _ = json . Marshal ( fill )
if _ , err := ImportCatalogBatch ( db , "thirdparty-csv" , fill , raw ); err != nil { t . Fatal ( err ) }
var title , mainSKU , url , pddTitle , shop , status string
_ = db . QueryRow ( `SELECT title,main_sku_code FROM shopee_products WHERE goods_id='S-1'` ). Scan ( & title , & mainSKU )
_ = db . QueryRow ( `SELECT url,title,shop_name,collect_status FROM pdd_products WHERE goods_id='P-1'` ). Scan ( & url , & pddTitle , & shop , & status )
if title != "人工标题" || mainSKU != "人工货号" || url != "https://manual.example/P-1" || pddTitle != "已采集标题" || shop != "已采集店铺" || status != "collected" { t . Fatalf ( "fill_missing 覆盖了已有字段:%q/%q/%q/%q/%q/%q" , title , mainSKU , url , pddTitle , shop , status ) }
}