fix: 修复不完整的商品目录迁移 (#144)

This commit is contained in:
chengma
2026-08-11 11:26:40 +08:00
parent cc595c164e
commit fef97cf5f3
4 changed files with 120 additions and 3 deletions
+30 -1
View File
@@ -19,7 +19,7 @@ import (
"cmautobuy/admin/spec"
)
const mysqlSchemaVersion = 9
const mysqlSchemaVersion = 10
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
@@ -531,10 +531,39 @@ func MigrateMySQL(db *sql.DB) error {
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 9, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
return fmt.Errorf("记录 MySQL schema v9 失败: %w", err)
}
current = 9
}
if current < 10 {
if err := migrateMySQLV10(db); err != nil {
return fmt.Errorf("执行 MySQL schema v10 失败: %w", err)
}
if err := checkMySQLV10Shape(db); err != nil {
return fmt.Errorf("MySQL schema v10 自检失败,未记录版本: %w", err)
}
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 10, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
return fmt.Errorf("记录 MySQL schema v10 失败: %w", err)
}
}
return CheckMySQLSchema(db)
}
// migrateMySQLV10 修复曾在 v8 开发中间状态记录过版本的数据库。
// v8 迁移本身逐项检查列和索引且回填只处理空值,因此可安全重放;
// 历史 v8/v9 版本记录保持不变,完整自检通过后再追加 v10。
func migrateMySQLV10(db *sql.DB) error {
if err := migrateMySQLV8(db); err != nil {
return fmt.Errorf("修复商品目录 SKU v8 结构失败: %w", err)
}
return nil
}
func checkMySQLV10Shape(db *sql.DB) error {
if err := checkMySQLV8Shape(db); err != nil {
return err
}
return checkMySQLV9Shape(db)
}
func migrateMySQLV9(db *sql.DB) error {
columns := []struct{ table, name, ddl string }{
{"shopee_products", "image_url", `ALTER TABLE shopee_products ADD COLUMN image_url VARCHAR(2048) NULL AFTER main_sku_code`},
@@ -468,6 +468,85 @@ func TestMySQLMigrate_V9形状错误不记版本(t *testing.T) {
}
}
func TestMySQLMigrate_已记录V9但缺少SKU来源字段时由V10修复(t *testing.T) {
db := openMySQLMigrationTestDB(t)
defer db.Close()
cleanMySQLTestSchema(t, db)
defer cleanMySQLTestSchema(t, db)
prepareMySQLV7(t, db)
now := "2026-08-11T00:00:00Z"
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,color,size,advice,parse_ok,sku_code,is_manual,source_observed_at,created_at,updated_at)
VALUES('REAL-1','S-1',' 黑色, M ','黑色','M','建议',1,'CODE-1',1,?,?,?)`, now, now, now)
if err := migrateMySQLV8(db); err != nil {
t.Fatal(err)
}
// 模拟 #141 开发中间状态:版本已继续推进到 v9,但两个后加入的 v8 字段不存在。
mustExec(t, db, `ALTER TABLE shopee_skus DROP COLUMN field_observed_at, DROP COLUMN field_sources`)
if err := migrateMySQLV9(db); err != nil {
t.Fatal(err)
}
mustExec(t, db, `INSERT INTO schema_migrations(version,applied_at) VALUES
(8,'2026-08-11T00:00:00Z'),(9,'2026-08-11T00:00:00Z')`)
if err := MigrateMySQL(db); err != nil {
t.Fatal(err)
}
if err := MigrateMySQL(db); err != nil {
t.Fatalf("v10 重放失败: %v", err)
}
var internalID, externalID, key, color, size, advice, skuCode, colorSource, colorObserved string
var manual int
err := db.QueryRow(`SELECT sku_id,shopee_sku_id,spec_key,color,size,advice,sku_code,is_manual,
JSON_UNQUOTE(JSON_EXTRACT(field_sources,'$.color')),
JSON_UNQUOTE(JSON_EXTRACT(field_observed_at,'$.color')) FROM shopee_skus WHERE sku_id='REAL-1'`).
Scan(&internalID, &externalID, &key, &color, &size, &advice, &skuCode, &manual, &colorSource, &colorObserved)
if err != nil {
t.Fatal(err)
}
if internalID != "REAL-1" || externalID != "REAL-1" || key != "黑色, M" || color != "黑色" || size != "M" || advice != "建议" || skuCode != "CODE-1" || manual != 1 {
t.Fatalf("v10 修改了既有 SKU 业务字段:id=%q external=%q key=%q color=%q size=%q advice=%q code=%q manual=%d",
internalID, externalID, key, color, size, advice, skuCode, manual)
}
if colorSource != "report" || colorObserved != now {
t.Fatalf("v10 来源回填不正确:source=%q observed=%q", colorSource, colorObserved)
}
var versionCount int
if err := db.QueryRow(`SELECT COUNT(*) FROM schema_migrations WHERE version=10`).Scan(&versionCount); err != nil {
t.Fatal(err)
}
if versionCount != 1 {
t.Fatalf("v10 应只记录一次,实际 %d", versionCount)
}
}
func TestMySQLMigrate_V10执行后未记版本可继续收敛(t *testing.T) {
db := openMySQLMigrationTestDB(t)
defer db.Close()
cleanMySQLTestSchema(t, db)
defer cleanMySQLTestSchema(t, db)
prepareMySQLV8(t, db)
if err := migrateMySQLV9(db); err != nil {
t.Fatal(err)
}
mustExec(t, db, `INSERT INTO schema_migrations(version,applied_at) VALUES(9,'2026-08-11T00:00:00Z')`)
// 模拟 v10 的 DDL/回填已隐式提交、但版本号尚未记录时进程退出。
if err := migrateMySQLV10(db); err != nil {
t.Fatal(err)
}
if err := MigrateMySQL(db); err != nil {
t.Fatalf("v10 中断后重跑失败: %v", err)
}
var versionCount int
if err := db.QueryRow(`SELECT COUNT(*) FROM schema_migrations WHERE version=10`).Scan(&versionCount); err != nil {
t.Fatal(err)
}
if versionCount != 1 {
t.Fatalf("v10 应只记录一次,实际 %d", versionCount)
}
}
func openMySQLMigrationTestDB(t *testing.T) *sql.DB {
t.Helper()
if os.Getenv("CMAUTOBUY_MYSQL_TEST") != "1" {