feat: 展示蝦皮主图和店铺 (#142)
This commit is contained in:
@@ -19,7 +19,7 @@ import (
|
||||
"cmautobuy/admin/spec"
|
||||
)
|
||||
|
||||
const mysqlSchemaVersion = 8
|
||||
const mysqlSchemaVersion = 9
|
||||
|
||||
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
|
||||
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
@@ -519,10 +519,65 @@ func MigrateMySQL(db *sql.DB) error {
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 8, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v8 失败: %w", err)
|
||||
}
|
||||
current = 8
|
||||
}
|
||||
if current < 9 {
|
||||
if err := migrateMySQLV9(db); err != nil {
|
||||
return fmt.Errorf("执行 MySQL schema v9 失败: %w", err)
|
||||
}
|
||||
if err := checkMySQLV9Shape(db); err != nil {
|
||||
return fmt.Errorf("MySQL schema v9 自检失败,未记录版本: %w", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
return CheckMySQLSchema(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`},
|
||||
{"shopee_products", "shopee_shop_name", `ALTER TABLE shopee_products ADD COLUMN shopee_shop_name VARCHAR(500) NULL AFTER image_url`},
|
||||
{"shopee_products", "image_source", `ALTER TABLE shopee_products ADD COLUMN image_source VARCHAR(64) COLLATE utf8mb4_bin NULL AFTER shopee_shop_name`},
|
||||
{"shopee_products", "image_observed_at", `ALTER TABLE shopee_products ADD COLUMN image_observed_at VARCHAR(35) NULL AFTER image_source`},
|
||||
{"shopee_products", "image_is_manual", `ALTER TABLE shopee_products ADD COLUMN image_is_manual TINYINT NOT NULL DEFAULT 0 AFTER image_observed_at`},
|
||||
{"shopee_products", "shop_name_source", `ALTER TABLE shopee_products ADD COLUMN shop_name_source VARCHAR(64) COLLATE utf8mb4_bin NULL AFTER image_is_manual`},
|
||||
{"shopee_products", "shop_name_observed_at", `ALTER TABLE shopee_products ADD COLUMN shop_name_observed_at VARCHAR(35) NULL AFTER shop_name_source`},
|
||||
{"shopee_products", "shop_name_is_manual", `ALTER TABLE shopee_products ADD COLUMN shop_name_is_manual TINYINT NOT NULL DEFAULT 0 AFTER shop_name_observed_at`},
|
||||
{"catalog_import_runs", "shopee_fields_filled", `ALTER TABLE catalog_import_runs ADD COLUMN shopee_fields_filled INT UNSIGNED NOT NULL DEFAULT 0 AFTER shopee_updated`},
|
||||
{"catalog_import_runs", "shopee_fields_same_source_updated", `ALTER TABLE catalog_import_runs ADD COLUMN shopee_fields_same_source_updated INT UNSIGNED NOT NULL DEFAULT 0 AFTER shopee_fields_filled`},
|
||||
{"catalog_import_runs", "shopee_fields_manual_skipped", `ALTER TABLE catalog_import_runs ADD COLUMN shopee_fields_manual_skipped INT UNSIGNED NOT NULL DEFAULT 0 AFTER shopee_fields_same_source_updated`},
|
||||
{"catalog_import_runs", "shopee_fields_stale_skipped", `ALTER TABLE catalog_import_runs ADD COLUMN shopee_fields_stale_skipped INT UNSIGNED NOT NULL DEFAULT 0 AFTER shopee_fields_manual_skipped`},
|
||||
}
|
||||
for _, column := range columns {
|
||||
exists, err := mysqlColumnExists(db, column.table, column.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := db.Exec(column.ddl); err != nil {
|
||||
return fmt.Errorf("增加 %s.%s 失败: %w", column.table, column.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, constraint := range []struct{ name, ddl string }{
|
||||
{"chk_shopee_products_image_manual", `ALTER TABLE shopee_products ADD CONSTRAINT chk_shopee_products_image_manual CHECK (image_is_manual IN (0,1))`},
|
||||
{"chk_shopee_products_shop_manual", `ALTER TABLE shopee_products ADD CONSTRAINT chk_shopee_products_shop_manual CHECK (shop_name_is_manual IN (0,1))`},
|
||||
} {
|
||||
exists, err := mysqlConstraintExists(db, "shopee_products", constraint.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := db.Exec(constraint.ddl); err != nil {
|
||||
return fmt.Errorf("增加约束 %s 失败: %w", constraint.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateMySQLV8 保留 sku_id 作为内部主键,并增加可空的真实蝦皮 SKU ID。
|
||||
// 每个 DDL 都先检查形状,支持 MySQL 隐式提交后的中断重放。
|
||||
func migrateMySQLV8(db *sql.DB) error {
|
||||
@@ -1056,7 +1111,48 @@ func CheckMySQLSchema(db *sql.DB) error {
|
||||
if err := checkMySQLV7Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV8Shape(db)
|
||||
if err := checkMySQLV8Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV9Shape(db)
|
||||
}
|
||||
|
||||
func checkMySQLV9Shape(db *sql.DB) error {
|
||||
for _, column := range []struct {
|
||||
name string
|
||||
length int64
|
||||
nullable bool
|
||||
collation, def string
|
||||
}{
|
||||
{"image_url", 2048, true, "utf8mb4_0900_ai_ci", ""}, {"shopee_shop_name", 500, true, "utf8mb4_0900_ai_ci", ""},
|
||||
{"image_source", 64, true, "utf8mb4_bin", ""}, {"image_observed_at", 35, true, "utf8mb4_0900_ai_ci", ""},
|
||||
{"shop_name_source", 64, true, "utf8mb4_bin", ""}, {"shop_name_observed_at", 35, true, "utf8mb4_0900_ai_ci", ""},
|
||||
} {
|
||||
if err := checkMySQLVarcharColumn(db, "shopee_products", column.name, column.length, column.nullable, column.collation, column.def); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"image_is_manual", "shop_name_is_manual"} {
|
||||
var dataType, isNullable string
|
||||
var defaultValue sql.NullString
|
||||
err := db.QueryRow(`SELECT data_type,is_nullable,column_default FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='shopee_products' AND column_name=?`, name).Scan(&dataType, &isNullable, &defaultValue)
|
||||
if err != nil || dataType != "tinyint" || isNullable != "NO" || !defaultValue.Valid || defaultValue.String != "0" {
|
||||
return fmt.Errorf("蝦皮商品字段 %s 形状不正确", name)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"chk_shopee_products_image_manual", "chk_shopee_products_shop_manual"} {
|
||||
exists, err := mysqlConstraintExists(db, "shopee_products", name)
|
||||
if err != nil || !exists {
|
||||
return fmt.Errorf("蝦皮商品约束 %s 缺失", name)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"shopee_fields_filled", "shopee_fields_same_source_updated", "shopee_fields_manual_skipped", "shopee_fields_stale_skipped"} {
|
||||
exists, err := mysqlColumnExists(db, "catalog_import_runs", name)
|
||||
if err != nil || !exists {
|
||||
return fmt.Errorf("目录记录字段 %s 缺失", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMySQLV8Shape(db *sql.DB) error {
|
||||
|
||||
Reference in New Issue
Block a user