feat: 迁移 Admin Repository 到 MySQL 8 (#79)
This commit is contained in:
@@ -13,7 +13,7 @@ import (
|
||||
"cmautobuy/admin/config"
|
||||
)
|
||||
|
||||
const mysqlSchemaVersion = 1
|
||||
const mysqlSchemaVersion = 2
|
||||
|
||||
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
|
||||
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
@@ -29,6 +29,8 @@ func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
driverConfig.ReadTimeout = 30 * time.Second
|
||||
driverConfig.WriteTimeout = 30 * time.Second
|
||||
driverConfig.RejectReadOnly = true
|
||||
// 业务层用 RowsAffected 判断目标行是否存在;重复写入相同值也应算匹配到。
|
||||
driverConfig.ClientFoundRows = true
|
||||
driverConfig.Params = map[string]string{
|
||||
"time_zone": "'+00:00'",
|
||||
"sql_mode": "'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'",
|
||||
@@ -298,6 +300,17 @@ var mysqlSchemaV1 = []string{
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
}
|
||||
|
||||
// mysqlSchemaV2 增加首位管理员初始化的并发哨兵。MySQL DDL 会隐式提交,
|
||||
// 因此每条语句都必须可重放,并在全部成功后才记录版本。
|
||||
var mysqlSchemaV2 = []string{
|
||||
`CREATE TABLE IF NOT EXISTS admin_initialization_lock (
|
||||
id TINYINT PRIMARY KEY,
|
||||
CHECK (id = 1)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`INSERT INTO admin_initialization_lock (id) VALUES (1)
|
||||
ON DUPLICATE KEY UPDATE id = VALUES(id)`,
|
||||
}
|
||||
|
||||
// MigrateMySQL 建立或升级 MySQL schema。生产迁移只能在这里追加新版本。
|
||||
func MigrateMySQL(db *sql.DB) error {
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
@@ -319,19 +332,39 @@ func MigrateMySQL(db *sql.DB) error {
|
||||
return fmt.Errorf("执行 MySQL schema v1 第 %d 条失败: %w", i+1, err)
|
||||
}
|
||||
}
|
||||
if err := CheckMySQLSchema(db); err != nil {
|
||||
if err := checkMySQLSchema(db, requiredTables); err != nil {
|
||||
return fmt.Errorf("MySQL schema v1 自检失败,未记录版本: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`,
|
||||
1, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v1 失败: %w", err)
|
||||
}
|
||||
current = 1
|
||||
}
|
||||
if current < 2 {
|
||||
for i, statement := range mysqlSchemaV2 {
|
||||
if _, err := db.Exec(statement); err != nil {
|
||||
return fmt.Errorf("执行 MySQL schema v2 第 %d 条失败: %w", i+1, err)
|
||||
}
|
||||
}
|
||||
if err := CheckMySQLSchema(db); err != nil {
|
||||
return fmt.Errorf("MySQL schema v2 自检失败,未记录版本: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`,
|
||||
2, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v2 失败: %w", err)
|
||||
}
|
||||
}
|
||||
return CheckMySQLSchema(db)
|
||||
}
|
||||
|
||||
// CheckMySQLSchema 确认所有业务表和关键追加列存在。
|
||||
func CheckMySQLSchema(db *sql.DB) error {
|
||||
mysqlRequiredTables := append(append([]string{}, requiredTables...), "admin_initialization_lock")
|
||||
return checkMySQLSchema(db, mysqlRequiredTables)
|
||||
}
|
||||
|
||||
func checkMySQLSchema(db *sql.DB, tables []string) error {
|
||||
rows, err := db.Query(`SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'`)
|
||||
if err != nil {
|
||||
@@ -350,7 +383,7 @@ func CheckMySQLSchema(db *sql.DB) error {
|
||||
return err
|
||||
}
|
||||
var missing []string
|
||||
for _, table := range requiredTables {
|
||||
for _, table := range tables {
|
||||
if !existing[table] {
|
||||
missing = append(missing, table)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user