@@ -20,7 +20,7 @@ import (
|
||||
"cmautobuy/admin/spec"
|
||||
)
|
||||
|
||||
const mysqlSchemaVersion = 14
|
||||
const mysqlSchemaVersion = 17
|
||||
|
||||
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
|
||||
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
@@ -592,10 +592,89 @@ func MigrateMySQL(db *sql.DB) error {
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 14, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v14 失败: %w", err)
|
||||
}
|
||||
current = 14
|
||||
}
|
||||
if current < 15 {
|
||||
if err := migrateMySQLV15(db); err != nil {
|
||||
return fmt.Errorf("执行 MySQL schema v15 失败: %w", err)
|
||||
}
|
||||
if err := checkMySQLV15Shape(db); err != nil {
|
||||
return fmt.Errorf("MySQL schema v15 自检失败,未记录版本: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 15, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v15 失败: %w", err)
|
||||
}
|
||||
current = 15
|
||||
}
|
||||
if current < 16 {
|
||||
if err := migrateMySQLV16(db); err != nil {
|
||||
return fmt.Errorf("执行 MySQL schema v16 失败: %w", err)
|
||||
}
|
||||
if err := checkMySQLV16Shape(db); err != nil {
|
||||
return fmt.Errorf("MySQL schema v16 自检失败,未记录版本: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 16, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v16 失败: %w", err)
|
||||
}
|
||||
current = 16
|
||||
}
|
||||
if current < 17 {
|
||||
if err := migrateMySQLV17(db); err != nil {
|
||||
return fmt.Errorf("执行 MySQL schema v17 失败: %w", err)
|
||||
}
|
||||
if err := checkMySQLV17Shape(db); err != nil {
|
||||
return fmt.Errorf("MySQL schema v17 自检失败,未记录版本: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 17, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v17 失败: %w", err)
|
||||
}
|
||||
}
|
||||
return CheckMySQLSchema(db)
|
||||
}
|
||||
|
||||
// migrateMySQLV15 以低优先级补全历史 SYB 商品的店铺和图片。
|
||||
func migrateMySQLV15(db *sql.DB) error {
|
||||
return backfillSybProductMetadata(db)
|
||||
}
|
||||
|
||||
// migrateMySQLV16 为历史 SYB 数据补建格式明确的低优先级蝦皮 SKU。
|
||||
func migrateMySQLV16(db *sql.DB) error {
|
||||
return backfillSybShopeeSKUs(db)
|
||||
}
|
||||
|
||||
// migrateMySQLV17 为蝦皮商品增加可恢复的软删除标记。
|
||||
func migrateMySQLV17(db *sql.DB) error {
|
||||
for _, column := range []struct{ name, ddl string }{
|
||||
{"deleted_at", `ALTER TABLE shopee_products ADD COLUMN deleted_at VARCHAR(35) NULL AFTER pdd_goods_id`},
|
||||
{"deleted_by_user_id", `ALTER TABLE shopee_products ADD COLUMN deleted_by_user_id VARCHAR(191) COLLATE utf8mb4_bin NULL AFTER deleted_at`},
|
||||
} {
|
||||
exists, err := mysqlColumnExists(db, "shopee_products", column.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := db.Exec(column.ddl); err != nil {
|
||||
return fmt.Errorf("增加 shopee_products.%s 失败: %w", column.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if exists, err := mysqlIndexExists(db, "shopee_products", "idx_shopee_products_deleted"); err != nil {
|
||||
return err
|
||||
} else if !exists {
|
||||
if _, err := db.Exec(`ALTER TABLE shopee_products ADD INDEX idx_shopee_products_deleted (deleted_at,updated_at,goods_id)`); err != nil {
|
||||
return fmt.Errorf("增加蝦皮商品删除状态索引失败: %w", err)
|
||||
}
|
||||
}
|
||||
if exists, err := mysqlConstraintExists(db, "shopee_products", "fk_shopee_products_deleted_by"); err != nil {
|
||||
return err
|
||||
} else if !exists {
|
||||
if _, err := db.Exec(`ALTER TABLE shopee_products ADD CONSTRAINT fk_shopee_products_deleted_by FOREIGN KEY (deleted_by_user_id) REFERENCES users(user_id) ON DELETE SET NULL`); err != nil {
|
||||
return fmt.Errorf("增加蝦皮商品删除人外键失败: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateMySQLV14 增加顺运宝同步店铺准入表和审计统计。
|
||||
func migrateMySQLV14(db *sql.DB) error {
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS syb_allowed_shops (
|
||||
@@ -1579,7 +1658,59 @@ func CheckMySQLSchema(db *sql.DB) error {
|
||||
if err := checkMySQLV13Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV14Shape(db)
|
||||
if err := checkMySQLV14Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkMySQLV15Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkMySQLV16Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV17Shape(db)
|
||||
}
|
||||
|
||||
func checkMySQLV15Shape(db *sql.DB) error {
|
||||
rows, err := db.Query(`SELECT image_source,image_observed_at,image_is_manual,
|
||||
shop_name_source,shop_name_observed_at,shop_name_is_manual
|
||||
FROM shopee_products LIMIT 0`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("蝦皮商品店铺与图片来源字段缺失: %w", err)
|
||||
}
|
||||
return rows.Close()
|
||||
}
|
||||
|
||||
func checkMySQLV16Shape(db *sql.DB) error {
|
||||
rows, err := db.Query(`SELECT spec_key,color,size,advice,parse_ok,source,field_sources,field_observed_at FROM shopee_skus LIMIT 0`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("蝦皮 SKU 解析与来源字段缺失: %w", err)
|
||||
}
|
||||
return rows.Close()
|
||||
}
|
||||
|
||||
func checkMySQLV17Shape(db *sql.DB) error {
|
||||
if err := checkMySQLVarcharColumn(db, "shopee_products", "deleted_at", 35, true, "utf8mb4_0900_ai_ci", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkMySQLVarcharColumn(db, "shopee_products", "deleted_by_user_id", 191, true, "utf8mb4_bin", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range []struct{ kind, name string }{{"index", "idx_shopee_products_deleted"}, {"constraint", "fk_shopee_products_deleted_by"}} {
|
||||
var exists bool
|
||||
var err error
|
||||
if item.kind == "index" {
|
||||
exists, err = mysqlIndexExists(db, "shopee_products", item.name)
|
||||
} else {
|
||||
exists, err = mysqlConstraintExists(db, "shopee_products", item.name)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("蝦皮商品软删除%s %s 缺失", item.kind, item.name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMySQLV14Shape(db *sql.DB) error {
|
||||
|
||||
Reference in New Issue
Block a user