iferr:=db.QueryRow(`SELECT spec_key FROM syb_orders WHERE syb_id='SYB-1'`).Scan(&key);err!=nil||key!="黑色, M"{
t.Fatalf("规格回填=%q err=%v",key,err)
}
varemptysql.NullString
iferr:=db.QueryRow(`SELECT spec_key FROM syb_orders WHERE syb_id='SYB-3'`).Scan(&empty);err!=nil||empty.Valid{
t.Fatalf("空规格必须保持 NULL: %+v err=%v",empty,err)
}
varsourcestring
iferr:=db.QueryRow(`SELECT source FROM shopee_products WHERE goods_id='SKELETON-1'`).Scan(&source);err!=nil||source!="syb"{
t.Fatalf("骨架来源=%q err=%v",source,err)
}
varmappingCountint
iferr:=db.QueryRow(`SELECT COUNT(*) FROM spec_mappings WHERE shopee_goods_id='REPORT-1' AND spec_key='黑色, M' AND pdd_goods_id='PDD-1'`).Scan(&mappingCount);err!=nil||mappingCount!=1{
mustExec(t,db,`UPDATE syb_orders SET spec_key=? WHERE syb_id='SYB-A'`,key)
}},
{"全部完成版本未记录",func(t*testing.T,db*sql.DB){
iferr:=migrateMySQLV3(db);err!=nil{
t.Fatal(err)
}
}},
}{
t.Run(tc.name,func(t*testing.T){
db:=openMySQLMigrationTestDB(t)
deferdb.Close()
cleanMySQLTestSchema(t,db)
defercleanMySQLTestSchema(t,db)
prepareMySQLV2(t,db)
now:="2026-08-10T03:00:00Z"
mustExec(t,db,`INSERT INTO syb_orders (syb_id,order_no,title,shopee_goods_id,product_spec,quantity,syb_data,created_at,updated_at) VALUES ('SYB-A','A','A','G-A','黑色,M',1,'{}',?,?),('SYB-B','B','B','G-B','白色,L',1,'{}',?,?)`,now,now,now,now)
tc.prepare(t,db)
iferr:=MigrateMySQL(db);err!=nil{
t.Fatalf("重跑失败: %v",err)
}
iferr:=MigrateMySQL(db);err!=nil{
t.Fatalf("再次重跑失败: %v",err)
}
varcountint
iferr:=db.QueryRow(`SELECT COUNT(*) FROM syb_orders WHERE spec_key IS NOT NULL`).Scan(&count);err!=nil||count!=2{
t.Fatalf("回填未收敛 count=%d err=%v",count,err)
}
})
}
}
funcTestMySQLMigrate_V3字段存在但约束缺失可收敛(t*testing.T){
db:=openMySQLMigrationTestDB(t)
deferdb.Close()
cleanMySQLTestSchema(t,db)
defercleanMySQLTestSchema(t,db)
prepareMySQLV2(t,db)
mustExec(t,db,`INSERT INTO shopee_products(goods_id,title,created_at,updated_at) VALUES ('EXISTING-1','存量商品','2026-08-10T00:00:00Z','2026-08-10T00:00:00Z')`)
mustExec(t,db,`INSERT INTO shopee_products(goods_id,title,source,created_at,updated_at) VALUES ('CHECK-1','检查','report','2026-08-10T00:00:00Z','2026-08-10T00:00:00Z')`)
if_,err:=db.Exec(`UPDATE shopee_products SET source='typo'`);err==nil{
mustExec(t,db,`INSERT INTO schema_migrations(version,applied_at) VALUES (3,'2026-08-10T00:00:00Z')`)
// 模拟 DDL 已完成、版本未记录。
mustExec(t,db,mysqlSchemaV4Decisions)
iferr:=MigrateMySQL(db);err!=nil{
t.Fatal(err)
}
iferr:=MigrateMySQL(db);err!=nil{
t.Fatalf("重跑失败: %v",err)
}
varversionsint
iferr:=db.QueryRow(`SELECT COUNT(*) FROM schema_migrations WHERE version=4`).Scan(&versions);err!=nil||versions!=1{
t.Fatalf("v4=%d err=%v",versions,err)
}
mustExec(t,db,`INSERT INTO spec_mapping_decisions(shopee_goods_id,spec_key,pdd_goods_id,rules_version,chosen_option_key,accepted,decided_at) VALUES('S','K','P','rules_v1','O',1,'2026-08-10T00:00:00Z')`)
if_,err:=db.Exec(`UPDATE spec_mapping_decisions SET accepted=2`);err==nil{
t.Fatal("accepted CHECK 必须拒绝 2")
}
}
funcTestMySQLMigrate_V4形状错误不记版本(t*testing.T){
db:=openMySQLMigrationTestDB(t)
deferdb.Close()
cleanMySQLTestSchema(t,db)
defercleanMySQLTestSchema(t,db)
prepareMySQLV2(t,db)
iferr:=migrateMySQLV3(db);err!=nil{
t.Fatal(err)
}
mustExec(t,db,`INSERT INTO schema_migrations(version,applied_at) VALUES (3,'2026-08-10T00:00:00Z')`)
mustExec(t,db,strings.Replace(mysqlSchemaV4Decisions,"CHECK (accepted IN (0,1))","CHECK (accepted IN (0,1,2))",1))
iferr:=MigrateMySQL(db);err==nil{
t.Fatal("错误 CHECK 必须阻止 v4")
}
varcountint
db.QueryRow(`SELECT COUNT(*) FROM schema_migrations WHERE version=4`).Scan(&count)
iferr:=db.QueryRow(`SELECT COUNT(*) FROM schema_migrations WHERE version=5`).Scan(&versions);err!=nil||versions!=1{
t.Fatalf("v5=%d err=%v",versions,err)
}
now:="2026-08-10T00:00:00Z"
mustExec(t,db,`INSERT INTO tasks(task_id,task_type,status,pdd_goods_url,created_at,updated_at) VALUES('OLD','collect','pending','https://example.invalid',?,?)`,now,now)
varmodestring
iferr:=db.QueryRow(`SELECT execution_mode FROM tasks WHERE task_id='OLD'`).Scan(&mode);err!=nil||mode!="dry_run"{
t.Fatalf("历史/缺省任务模式=%q err=%v",mode,err)
}
if_,err:=db.Exec(`UPDATE tasks SET execution_mode='invalid' WHERE task_id='OLD'`);err==nil{
t.Fatal("执行模式 CHECK 必须拒绝非法值")
}
if_,err:=db.Exec(`UPDATE tasks SET execution_mode='live' WHERE task_id='OLD'`);err==nil{
t.Fatal("缺少确认审计的 live 必须被拒绝")
}
if_,err:=db.Exec(`UPDATE tasks SET live_confirmed_by='U',live_confirmed_at=? WHERE task_id='OLD'`,now);err==nil{
mustExec(t,db,`INSERT INTO schema_migrations(version,applied_at) VALUES (5,'2026-08-10T00:00:00Z')`)
now:="2026-08-10T00:00:00Z"
mustExec(t,db,`INSERT INTO tasks(task_id,task_type,status,pdd_goods_url,created_at,updated_at) VALUES('HISTORY','collect','pending','https://example.invalid',?,?)`,now,now)
// 模拟 DDL 已提交但版本号尚未写入,再启动必须能够收敛。
iferr:=migrateMySQLV6(db);err!=nil{
t.Fatal(err)
}
iferr:=MigrateMySQL(db);err!=nil{
t.Fatal(err)
}
iferr:=MigrateMySQL(db);err!=nil{
t.Fatalf("v6 重跑失败: %v",err)
}
varversionsint
iferr:=db.QueryRow(`SELECT COUNT(*) FROM schema_migrations WHERE version=6`).Scan(&versions);err!=nil||versions!=1{
t.Fatalf("v6=%d err=%v",versions,err)
}
varcreatorsql.NullString
iferr:=db.QueryRow(`SELECT created_by_user_id FROM tasks WHERE task_id='HISTORY'`).Scan(&creator);err!=nil||creator.Valid{
mustExec(t,db,`INSERT INTO shopee_products(goods_id,title,source,created_at,updated_at) VALUES('S-1','商品','report',?,?)`,now,now)
mustExec(t,db,`INSERT INTO shopee_skus(sku_id,goods_id,spec_raw,created_at,updated_at) VALUES('A','S-1','黑色,M',?,?),('B','S-1','黑色,M',?,?)`,now,now,now,now)
mustExec(t,db,`INSERT INTO schema_migrations(version,applied_at) VALUES(8,'2026-08-11T00:00:00Z')`)
iferr:=migrateMySQLV9(db);err!=nil{
t.Fatal(err)
}
iferr:=MigrateMySQL(db);err!=nil{
t.Fatal(err)
}
iferr:=MigrateMySQL(db);err!=nil{
t.Fatalf("v9 重放失败: %v",err)
}
mustExec(t,db,`INSERT INTO shopee_products(goods_id,title,source,image_url,shopee_shop_name,image_is_manual,shop_name_is_manual,created_at,updated_at) VALUES('S','商品','api','https://example.com/a.jpg','店铺',0,0,'2026-08-11T00:00:00Z','2026-08-11T00:00:00Z')`)