@@ -20,7 +20,7 @@ import (
|
||||
"cmautobuy/admin/spec"
|
||||
)
|
||||
|
||||
const mysqlSchemaVersion = 17
|
||||
const mysqlSchemaVersion = 18
|
||||
|
||||
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
|
||||
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
@@ -628,6 +628,18 @@ func MigrateMySQL(db *sql.DB) error {
|
||||
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)
|
||||
}
|
||||
current = 17
|
||||
}
|
||||
if current < 18 {
|
||||
if err := migrateMySQLV18(db); err != nil {
|
||||
return fmt.Errorf("执行 MySQL schema v18 失败: %w", err)
|
||||
}
|
||||
if err := checkMySQLV18Shape(db); err != nil {
|
||||
return fmt.Errorf("MySQL schema v18 自检失败,未记录版本: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 18, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v18 失败: %w", err)
|
||||
}
|
||||
}
|
||||
return CheckMySQLSchema(db)
|
||||
}
|
||||
@@ -675,6 +687,114 @@ func migrateMySQLV17(db *sql.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateMySQLV18 建立跨渠道店铺身份,保留旧 SYB 准入表作为回退依据。
|
||||
func migrateMySQLV18(db *sql.DB) error {
|
||||
statements := []string{
|
||||
`CREATE TABLE IF NOT EXISTS shops (
|
||||
shop_id VARCHAR(191) COLLATE utf8mb4_bin PRIMARY KEY,
|
||||
display_name VARCHAR(500) NOT NULL,
|
||||
normalized_name VARCHAR(500) COLLATE utf8mb4_bin NOT NULL,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
created_by_user_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
updated_at VARCHAR(35) NOT NULL,
|
||||
UNIQUE KEY uq_shops_name (normalized_name),
|
||||
KEY idx_shops_enabled (enabled,normalized_name),
|
||||
CONSTRAINT fk_shops_user FOREIGN KEY (created_by_user_id) REFERENCES users(user_id),
|
||||
CONSTRAINT chk_shops_enabled CHECK (enabled IN (0,1))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS shop_channel_aliases (
|
||||
alias_id VARCHAR(191) COLLATE utf8mb4_bin PRIMARY KEY,
|
||||
shop_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
channel VARCHAR(16) COLLATE utf8mb4_bin NOT NULL,
|
||||
alias_name VARCHAR(500) NOT NULL,
|
||||
normalized_alias VARCHAR(500) COLLATE utf8mb4_bin NOT NULL,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
updated_at VARCHAR(35) NOT NULL,
|
||||
UNIQUE KEY uq_shop_channel_alias (channel,normalized_alias),
|
||||
KEY idx_shop_alias_shop (shop_id,channel,enabled),
|
||||
CONSTRAINT fk_shop_alias_shop FOREIGN KEY (shop_id) REFERENCES shops(shop_id) ON DELETE CASCADE,
|
||||
CONSTRAINT chk_shop_alias_channel CHECK (channel IN ('syb','shopee')),
|
||||
CONSTRAINT chk_shop_alias_enabled CHECK (enabled IN (0,1))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := db.Exec(statement); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, column := range []struct{ table, name, ddl string }{
|
||||
{"syb_orders", "shop_id", `ALTER TABLE syb_orders ADD COLUMN shop_id VARCHAR(191) COLLATE utf8mb4_bin NULL AFTER order_no`},
|
||||
{"shopee_products", "shop_id", `ALTER TABLE shopee_products ADD COLUMN shop_id VARCHAR(191) COLLATE utf8mb4_bin NULL AFTER goods_id`},
|
||||
} {
|
||||
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 _, index := range []struct{ table, name, ddl string }{
|
||||
{"syb_orders", "idx_syb_orders_shop_id", `ALTER TABLE syb_orders ADD INDEX idx_syb_orders_shop_id (shop_id)`},
|
||||
{"shopee_products", "idx_shopee_products_shop_id", `ALTER TABLE shopee_products ADD INDEX idx_shopee_products_shop_id (shop_id)`},
|
||||
} {
|
||||
exists, err := mysqlIndexExists(db, index.table, index.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := db.Exec(index.ddl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, fk := range []struct{ table, name, ddl string }{
|
||||
{"syb_orders", "fk_syb_orders_shop", `ALTER TABLE syb_orders ADD CONSTRAINT fk_syb_orders_shop FOREIGN KEY (shop_id) REFERENCES shops(shop_id) ON DELETE SET NULL`},
|
||||
{"shopee_products", "fk_shopee_products_shop", `ALTER TABLE shopee_products ADD CONSTRAINT fk_shopee_products_shop FOREIGN KEY (shop_id) REFERENCES shops(shop_id) ON DELETE SET NULL`},
|
||||
} {
|
||||
exists, err := mysqlConstraintExists(db, fk.table, fk.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := db.Exec(fk.ddl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// 旧准入项一对一迁移成业务店铺;旧 enabled 只控制 SYB 渠道,不停用业务店铺。
|
||||
if _, err := db.Exec(`INSERT INTO shops(shop_id,display_name,normalized_name,enabled,created_by_user_id,created_at,updated_at)
|
||||
SELECT shop_id,shop_name,normalized_name,1,created_by_user_id,created_at,updated_at FROM syb_allowed_shops
|
||||
ON DUPLICATE KEY UPDATE display_name=VALUES(display_name),updated_at=VALUES(updated_at)`); err != nil {
|
||||
return fmt.Errorf("迁移旧同步店铺失败: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO shop_channel_aliases(alias_id,shop_id,channel,alias_name,normalized_alias,enabled,created_at,updated_at)
|
||||
SELECT CONCAT('legacy-syb-',shop_id),shop_id,'syb',shop_name,normalized_name,enabled,created_at,updated_at FROM syb_allowed_shops
|
||||
ON DUPLICATE KEY UPDATE shop_id=VALUES(shop_id),alias_name=VALUES(alias_name),enabled=VALUES(enabled),updated_at=VALUES(updated_at)`); err != nil {
|
||||
return fmt.Errorf("迁移 SYB 店铺别名失败: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO shop_channel_aliases(alias_id,shop_id,channel,alias_name,normalized_alias,enabled,created_at,updated_at)
|
||||
SELECT CONCAT('legacy-shopee-',shop_id),shop_id,'shopee',shop_name,normalized_name,1,created_at,updated_at FROM syb_allowed_shops
|
||||
ON DUPLICATE KEY UPDATE shop_id=VALUES(shop_id),alias_name=VALUES(alias_name),updated_at=VALUES(updated_at)`); err != nil {
|
||||
return fmt.Errorf("迁移蝦皮店铺别名失败: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`UPDATE syb_orders so JOIN shop_channel_aliases a
|
||||
ON a.channel='syb' AND a.normalized_alias=TRIM(so.shop_name) COLLATE utf8mb4_bin
|
||||
SET so.shop_id=a.shop_id WHERE so.shop_id IS NULL`); err != nil {
|
||||
return fmt.Errorf("回填顺运宝业务店铺失败: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`UPDATE shopee_products sp JOIN shop_channel_aliases a
|
||||
ON a.channel='shopee' AND a.normalized_alias=TRIM(sp.shopee_shop_name) COLLATE utf8mb4_bin
|
||||
SET sp.shop_id=a.shop_id WHERE sp.shop_id IS 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 (
|
||||
@@ -1667,7 +1787,10 @@ func CheckMySQLSchema(db *sql.DB) error {
|
||||
if err := checkMySQLV16Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV17Shape(db)
|
||||
if err := checkMySQLV17Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV18Shape(db)
|
||||
}
|
||||
|
||||
func checkMySQLV15Shape(db *sql.DB) error {
|
||||
@@ -1713,6 +1836,39 @@ func checkMySQLV17Shape(db *sql.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMySQLV18Shape(db *sql.DB) error {
|
||||
if err := checkMySQLSchema(db, []string{"shops", "shop_channel_aliases"}); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, column := range []struct{ table, name string }{{"syb_orders", "shop_id"}, {"shopee_products", "shop_id"}} {
|
||||
if err := checkMySQLVarcharColumn(db, column.table, column.name, 191, true, "utf8mb4_bin", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, item := range []struct{ table, name string }{
|
||||
{"shops", "uq_shops_name"}, {"shops", "idx_shops_enabled"},
|
||||
{"shop_channel_aliases", "uq_shop_channel_alias"}, {"shop_channel_aliases", "idx_shop_alias_shop"},
|
||||
{"syb_orders", "idx_syb_orders_shop_id"}, {"shopee_products", "idx_shopee_products_shop_id"},
|
||||
} {
|
||||
exists, err := mysqlIndexExists(db, item.table, item.name)
|
||||
if err != nil || !exists {
|
||||
return fmt.Errorf("店铺索引 %s.%s 缺失: %v", item.table, item.name, err)
|
||||
}
|
||||
}
|
||||
for _, item := range []struct{ table, name string }{
|
||||
{"shops", "fk_shops_user"}, {"shops", "chk_shops_enabled"},
|
||||
{"shop_channel_aliases", "fk_shop_alias_shop"}, {"shop_channel_aliases", "chk_shop_alias_channel"},
|
||||
{"shop_channel_aliases", "chk_shop_alias_enabled"}, {"syb_orders", "fk_syb_orders_shop"},
|
||||
{"shopee_products", "fk_shopee_products_shop"},
|
||||
} {
|
||||
exists, err := mysqlConstraintExists(db, item.table, item.name)
|
||||
if err != nil || !exists {
|
||||
return fmt.Errorf("店铺约束 %s.%s 缺失: %v", item.table, item.name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMySQLV14Shape(db *sql.DB) error {
|
||||
if err := checkMySQLSchema(db, []string{"syb_allowed_shops"}); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user