// Package migration 基于 golang-migrate 提供数据库结构迁移能力。 // 迁移脚本以 go:embed 打包进二进制,通过独立命令 cmd/migrate 手动执行;服务启动流程不触发迁移。 package migration import ( "database/sql" "embed" "errors" "fmt" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/setting" _ "github.com/go-sql-driver/mysql" "github.com/golang-migrate/migrate/v4" migratemysql "github.com/golang-migrate/migrate/v4/database/mysql" "github.com/golang-migrate/migrate/v4/source/iofs" ) // migrationsDir 迁移脚本所在目录:脚本与 migration.go 同级,故为当前目录。 const migrationsDir = "." //go:embed *.sql var migrationFS embed.FS // dsn 拼装迁移专用 DSN。 // 必须开启 multiStatements:单个迁移文件内含多条 CREATE TABLE / CREATE INDEX 语句, // 未开启时 MySQL 会拒绝执行。该参数与 gorm 使用的 DSN 不同,故此处单独拼装。 func dsn(s *setting.MysqlSettingS) string { return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true", s.UserName, s.Password, s.Host, s.Port, s.DBName, ) } // newMigrate 创建 migrate 实例;返回的 close 用于释放底层数据库连接。 func newMigrate(s *setting.MysqlSettingS) (*migrate.Migrate, func() error, error) { if s == nil { return nil, nil, errors.New("mysql setting is nil") } if s.DBName == "" { return nil, nil, errors.New("mysql DBName is empty") } source, err := iofs.New(migrationFS, migrationsDir) if err != nil { return nil, nil, fmt.Errorf("load embedded migrations: %w", err) } db, err := sql.Open("mysql", dsn(s)) if err != nil { return nil, nil, fmt.Errorf("open mysql: %w", err) } closeFn := func() error { return db.Close() } driver, err := migratemysql.WithInstance(db, &migratemysql.Config{}) if err != nil { _ = closeFn() return nil, nil, fmt.Errorf("create mysql migrate driver: %w", err) } m, err := migrate.NewWithInstance("iofs", source, "mysql", driver) if err != nil { _ = closeFn() return nil, nil, fmt.Errorf("create migrate instance: %w", err) } return m, closeFn, nil } // Up 执行到最新版本;无待执行迁移(ErrNoChange)视为成功。 func Up(s *setting.MysqlSettingS) error { m, closeFn, err := newMigrate(s) if err != nil { return err } defer closeFn() if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { return fmt.Errorf("migrate up: %w", err) } return nil } // Down 全量回滚所有已应用的迁移。 func Down(s *setting.MysqlSettingS) error { m, closeFn, err := newMigrate(s) if err != nil { return err } defer closeFn() if err := m.Down(); err != nil && !errors.Is(err, migrate.ErrNoChange) { return fmt.Errorf("migrate down: %w", err) } return nil } // Steps 按步数迁移:n > 0 向上 n 步,n < 0 向下 n 步。 func Steps(s *setting.MysqlSettingS, n int) error { m, closeFn, err := newMigrate(s) if err != nil { return err } defer closeFn() if err := m.Steps(n); err != nil && !errors.Is(err, migrate.ErrNoChange) { return fmt.Errorf("migrate steps %d: %w", n, err) } return nil } // Version 返回当前迁移版本号、是否为脏库状态。未执行过任何迁移时返回 0。 func Version(s *setting.MysqlSettingS) (uint, bool, error) { m, closeFn, err := newMigrate(s) if err != nil { return 0, false, err } defer closeFn() version, dirty, err := m.Version() if err != nil { if errors.Is(err, migrate.ErrNilVersion) { return 0, false, nil } return 0, false, fmt.Errorf("migrate version: %w", err) } return version, dirty, nil } // Force 强制设置版本号并清除脏标记,用于修复迁移中途失败导致的 dirty 状态。 func Force(s *setting.MysqlSettingS, version int) error { if version < 0 { return fmt.Errorf("force version must not be negative: %d", version) } m, closeFn, err := newMigrate(s) if err != nil { return err } defer closeFn() if err := m.Force(version); err != nil { return fmt.Errorf("migrate force %d: %w", version, err) } return nil } // Run 按 action 执行迁移动作;arg 仅 steps / force 使用。 // action 取值:up、down、steps、force。 func Run(s *setting.MysqlSettingS, action string, arg int) error { switch action { case "up": return Up(s) case "down": return Down(s) case "steps": return Steps(s, arg) case "force": return Force(s, arg) default: return fmt.Errorf("unknown migration action: %s", action) } }