first commit

This commit is contained in:
2026-09-11 01:41:52 +08:00
commit 9f22e6ccae
263 changed files with 28935 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
// 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)
}
}