feat: 完善 SYB 蝦皮主数据与软删除 (#203 #204 #205)

This commit is contained in:
chengma
2026-08-13 10:39:21 +08:00
parent 3af9bf245a
commit ad1ccc2c60
23 changed files with 864 additions and 90 deletions
+133 -2
View File
@@ -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 {