package migration import ( "fmt" "os" "time" "git.toowon.com/jimmy/go-common/config" "gorm.io/driver/mysql" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" ) // RunMigrationsFromConfig 从配置文件运行迁移(便捷方法) // // 注意:推荐使用独立的迁移工具(templates/migrate/main.go),而不是在应用代码中直接调用。 // 独立工具可以实现零耦合、独立部署。 // // 此方法主要用于: // 1. 独立迁移工具内部调用(推荐) // 2. 简单场景下在应用启动时调用(不推荐,会导致耦合) // // 用法: // // import "git.toowon.com/jimmy/go-common/migration" // migration.RunMigrationsFromConfig("config.json", "migrations") func RunMigrationsFromConfig(configFile, migrationsDir string) error { // 加载配置 cfg, err := loadConfigFromFileOrEnv(configFile) if err != nil { return fmt.Errorf("加载配置失败: %w", err) } // 连接数据库 db, err := connectDB(cfg) if err != nil { return fmt.Errorf("连接数据库失败: %w", err) } // 创建迁移器 migrator := NewMigrator(db) // 加载迁移文件 migrations, err := LoadMigrationsFromFiles(migrationsDir, "*.sql") if err != nil { return fmt.Errorf("加载迁移文件失败: %w", err) } if len(migrations) == 0 { fmt.Printf("在目录 '%s' 中没有找到迁移文件\n", migrationsDir) return nil } migrator.AddMigrations(migrations...) // 执行迁移 if err := migrator.Up(); err != nil { return fmt.Errorf("执行迁移失败: %w", err) } fmt.Println("✓ 迁移执行成功") return nil } // RunMigrationsFromConfigWithCommand 从配置文件运行迁移(支持命令) // command: "up", "down", "status" func RunMigrationsFromConfigWithCommand(configFile, migrationsDir, command string) error { // 加载配置 cfg, err := loadConfigFromFileOrEnv(configFile) if err != nil { return fmt.Errorf("加载配置失败: %w", err) } // 连接数据库 db, err := connectDB(cfg) if err != nil { return fmt.Errorf("连接数据库失败: %w", err) } // 创建迁移器 migrator := NewMigrator(db) // 加载迁移文件 migrations, err := LoadMigrationsFromFiles(migrationsDir, "*.sql") if err != nil { return fmt.Errorf("加载迁移文件失败: %w", err) } if len(migrations) == 0 { fmt.Printf("在目录 '%s' 中没有找到迁移文件\n", migrationsDir) return nil } migrator.AddMigrations(migrations...) // 执行命令 switch command { case "up": if err := migrator.Up(); err != nil { return fmt.Errorf("执行迁移失败: %w", err) } fmt.Println("✓ 迁移执行成功") case "down": if err := migrator.Down(); err != nil { return fmt.Errorf("回滚迁移失败: %w", err) } fmt.Println("✓ 迁移回滚成功") case "status": status, err := migrator.Status() if err != nil { return fmt.Errorf("获取迁移状态失败: %w", err) } printMigrationStatus(status) default: return fmt.Errorf("未知命令: %s (支持: up, down, status)", command) } return nil } // loadConfigFromFileOrEnv 从文件或环境变量加载配置 func loadConfigFromFileOrEnv(configFile string) (*config.Config, error) { // 优先从环境变量加载 if dbURL := os.Getenv("DATABASE_URL"); dbURL != "" { return &config.Config{ Database: &config.DatabaseConfig{ DSN: dbURL, }, }, nil } // 尝试从配置文件加载 if configFile != "" { if _, err := os.Stat(configFile); err == nil { return config.LoadFromFile(configFile) } } // 尝试默认路径 defaultPaths := []string{"config.json", "../config.json"} for _, path := range defaultPaths { if _, err := os.Stat(path); err == nil { return config.LoadFromFile(path) } } return nil, fmt.Errorf("未找到配置文件,也未设置环境变量 DATABASE_URL") } // connectDB 连接数据库 func connectDB(cfg *config.Config) (*gorm.DB, error) { if cfg.Database == nil { return nil, fmt.Errorf("数据库配置为空") } dsn, err := cfg.GetDatabaseDSN() if err != nil { return nil, err } var db *gorm.DB switch cfg.Database.Type { case "mysql": db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) case "postgres": db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) case "sqlite": db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{}) default: return nil, fmt.Errorf("不支持的数据库类型: %s", cfg.Database.Type) } if err != nil { return nil, err } // 配置连接池 sqlDB, err := db.DB() if err != nil { return nil, err } sqlDB.SetMaxOpenConns(10) sqlDB.SetMaxIdleConns(5) sqlDB.SetConnMaxLifetime(time.Hour) return db, nil } // printMigrationStatus 打印迁移状态 func printMigrationStatus(status []MigrationStatus) { if len(status) == 0 { fmt.Println("没有找到迁移") return } fmt.Println("\n迁移状态:") fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") fmt.Printf("%-20s %-40s %-10s\n", "版本", "描述", "状态") fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") for _, s := range status { statusText := "待执行" if s.Applied { statusText = "✓ 已应用" } fmt.Printf("%-20s %-40s %-10s\n", s.Version, s.Description, statusText) } fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") fmt.Println() }