feat: 建立全局店铺跨渠道管理 (#208 #209 #210 #211)

This commit is contained in:
chengma
2026-08-13 11:15:54 +08:00
parent 901b37a8cb
commit 8f707722ba
28 changed files with 1160 additions and 500 deletions
+158 -2
View File
@@ -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