package database import ( "context" "fmt" "log/slog" "time" "gorm.io/gorm" "rill/internal/model" ) // Migration 描述一次数据库结构变更,Version 必须唯一且递增。 type Migration struct { Version int Name string Up func(*gorm.DB) error } // migrations 按 Version 升序登记所有迁移,新增迁移只能追加。 var migrations = []Migration{ { Version: 1, Name: "create_notes", Up: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.Note{}) }, }, { Version: 2, Name: "create_user_groups", Up: func(tx *gorm.DB) error { if err := tx.AutoMigrate(&model.UserGroup{}); err != nil { return err } now := time.Now() seeds := []struct { id uint name string desc string }{ {id: model.GroupIDAdmin, name: "admin", desc: "Administrator group"}, {id: model.GroupIDUser, name: "user", desc: "Regular user group"}, } for _, seed := range seeds { if err := tx.Exec( "INSERT INTO user_groups (id, name, description, is_system, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", seed.id, seed.name, seed.desc, true, now, now, ).Error; err != nil { return err } } return nil }, }, { Version: 3, Name: "create_users", Up: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.User{}, &model.UserGroupMember{}) }, }, { Version: 4, Name: "seed_admin_user", Up: seedAdminUser, }, { Version: 5, Name: "translate_builtin_data", Up: func(tx *gorm.DB) error { statements := []string{ "UPDATE user_groups SET description = 'Administrator group' WHERE id = 0 AND description = '管理员组'", "UPDATE user_groups SET description = 'Regular user group' WHERE id = 1 AND description = '普通用户组'", "UPDATE users SET nickname = 'Administrator' WHERE username = 'admin' AND nickname = '管理员'", } for _, statement := range statements { if err := tx.Exec(statement).Error; err != nil { return err } } return nil }, }, { Version: 6, Name: "add_user_profile_fields", Up: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.User{}) }, }, { Version: 7, Name: "create_site_settings", Up: func(tx *gorm.DB) error { if err := tx.AutoMigrate(&model.SiteSetting{}); err != nil { return err } now := time.Now() return tx.Exec( "INSERT INTO site_settings (id, site_name, logo, footer, created_at, updated_at) VALUES (?, ?, '', '', ?, ?)", model.SiteSettingID, "Rill", now, now, ).Error }, }, { Version: 8, Name: "create_files", Up: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.File{}) }, }, { Version: 9, Name: "create_file_operations", Up: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.FileOperation{}) }, }, { Version: 10, Name: "create_nav_links", Up: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.NavLink{}, &model.NavLinkTranslation{}) }, }, } // schemaMigration 记录已应用的迁移版本。 type schemaMigration struct { Version int `gorm:"primaryKey"` Name string AppliedAt time.Time } func (schemaMigration) TableName() string { return "schema_migrations" } // Migrate 依次执行未应用的迁移,每个迁移在独立事务中完成并记录版本,可重复调用。 func Migrate(ctx context.Context, db *gorm.DB) error { tx := db.WithContext(ctx) if err := tx.AutoMigrate(&schemaMigration{}); err != nil { return fmt.Errorf("初始化迁移记录表失败: %w", err) } var applied []int if err := tx.Model(&schemaMigration{}).Pluck("version", &applied).Error; err != nil { return fmt.Errorf("读取迁移记录失败: %w", err) } done := make(map[int]bool, len(applied)) for _, version := range applied { done[version] = true } for _, m := range migrations { if done[m.Version] { continue } err := tx.Transaction(func(tx *gorm.DB) error { if err := m.Up(tx); err != nil { return err } return tx.Create(&schemaMigration{Version: m.Version, Name: m.Name, AppliedAt: time.Now()}).Error }) if err != nil { return fmt.Errorf("执行迁移 %d_%s 失败: %w", m.Version, m.Name, err) } slog.Info("数据库迁移已应用", "version", m.Version, "name", m.Name) } return nil }