@@ -20,7 +20,7 @@ import (
|
||||
"cmautobuy/admin/spec"
|
||||
)
|
||||
|
||||
const mysqlSchemaVersion = 18
|
||||
const mysqlSchemaVersion = 19
|
||||
|
||||
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
|
||||
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
@@ -640,6 +640,18 @@ func MigrateMySQL(db *sql.DB) error {
|
||||
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)
|
||||
}
|
||||
current = 18
|
||||
}
|
||||
if current < 19 {
|
||||
if err := migrateMySQLV19(db); err != nil {
|
||||
return fmt.Errorf("执行 MySQL schema v19 失败: %w", err)
|
||||
}
|
||||
if err := checkMySQLV19Shape(db); err != nil {
|
||||
return fmt.Errorf("MySQL schema v19 自检失败,未记录版本: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 19, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v19 失败: %w", err)
|
||||
}
|
||||
}
|
||||
return CheckMySQLSchema(db)
|
||||
}
|
||||
@@ -795,6 +807,51 @@ func migrateMySQLV18(db *sql.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateMySQLV19 把 v18 的渠道别名收敛成唯一店铺名称。
|
||||
// shop_channel_aliases 暂时作为兼容表保留,其内容完全由 shops 派生,不再允许独立配置。
|
||||
func migrateMySQLV19(db *sql.DB) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
// v18 同时存在全局启停和 SYB 启停。收敛时任一侧已停用都保持停用,
|
||||
// 避免升级后意外把原本禁止同步的店铺重新放入同步范围。
|
||||
if _, err := tx.Exec(`UPDATE shops s JOIN (
|
||||
SELECT shop_id,MIN(enabled) AS enabled FROM shop_channel_aliases
|
||||
WHERE channel='syb' GROUP BY shop_id
|
||||
) a ON a.shop_id=s.shop_id
|
||||
SET s.enabled=IF(s.enabled=1 AND a.enabled=1,1,0)`); err != nil {
|
||||
return fmt.Errorf("收敛店铺启停状态失败: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM shop_channel_aliases`); err != nil {
|
||||
return fmt.Errorf("清理旧渠道店铺名称失败: %w", err)
|
||||
}
|
||||
for _, channel := range []string{"syb", "shopee"} {
|
||||
if _, err := tx.Exec(`INSERT INTO shop_channel_aliases
|
||||
(alias_id,shop_id,channel,alias_name,normalized_alias,enabled,created_at,updated_at)
|
||||
SELECT CONCAT('v19-',?,'-',SHA2(shop_id,256)),shop_id,?,display_name,normalized_name,enabled,created_at,updated_at
|
||||
FROM shops`, channel, channel); err != nil {
|
||||
return fmt.Errorf("生成 %s 店铺兼容数据失败: %w", channel, err)
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(`UPDATE syb_orders SET shop_id=NULL`); err != nil {
|
||||
return fmt.Errorf("清理顺运宝旧店铺关联失败: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`UPDATE syb_orders so JOIN shops s
|
||||
ON s.normalized_name=TRIM(so.shop_name) COLLATE utf8mb4_bin SET so.shop_id=s.shop_id`); err != nil {
|
||||
return fmt.Errorf("按唯一店铺名称重建顺运宝关联失败: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`UPDATE shopee_products SET shop_id=NULL`); err != nil {
|
||||
return fmt.Errorf("清理蝦皮旧店铺关联失败: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`UPDATE shopee_products sp JOIN shops s
|
||||
ON s.normalized_name=TRIM(sp.shopee_shop_name) COLLATE utf8mb4_bin SET sp.shop_id=s.shop_id`); err != nil {
|
||||
return fmt.Errorf("按唯一店铺名称重建蝦皮关联失败: %w", err)
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// migrateMySQLV14 增加顺运宝同步店铺准入表和审计统计。
|
||||
func migrateMySQLV14(db *sql.DB) error {
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS syb_allowed_shops (
|
||||
@@ -1790,7 +1847,10 @@ func CheckMySQLSchema(db *sql.DB) error {
|
||||
if err := checkMySQLV17Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV18Shape(db)
|
||||
if err := checkMySQLV18Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV19Shape(db)
|
||||
}
|
||||
|
||||
func checkMySQLV15Shape(db *sql.DB) error {
|
||||
@@ -1869,6 +1929,22 @@ func checkMySQLV18Shape(db *sql.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMySQLV19Shape(db *sql.DB) error {
|
||||
var invalid int
|
||||
err := db.QueryRow(`SELECT COUNT(*) FROM shops s WHERE
|
||||
(SELECT COUNT(*) FROM shop_channel_aliases a WHERE a.shop_id=s.shop_id AND a.channel='syb')<>1 OR
|
||||
(SELECT COUNT(*) FROM shop_channel_aliases a WHERE a.shop_id=s.shop_id AND a.channel='shopee')<>1 OR
|
||||
EXISTS (SELECT 1 FROM shop_channel_aliases a WHERE a.shop_id=s.shop_id AND
|
||||
(a.alias_name<>s.display_name OR a.normalized_alias<>s.normalized_name OR a.enabled<>s.enabled))`).Scan(&invalid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查单一店铺名称兼容数据失败: %w", err)
|
||||
}
|
||||
if invalid != 0 {
|
||||
return fmt.Errorf("有 %d 个店铺的渠道兼容数据与唯一店铺名称不一致", invalid)
|
||||
}
|
||||
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