Files
rill/internal/database/migrate.go
T
kevin 4449250e97 新增头部导航链接配置与多语言支持
- 迁移 v10 新增 nav_links 与 nav_link_translations 两张表,文案按语言存储
- 公开 GET /api/nav-links 返回启用链接(按 sort/id 排序);管理员可增删改查与启停
- URL 仅允许站内路径、http(s) 与 mailto,拒绝 javascript: 等危险协议;译文替换与校验
- 前端删除写死的“主页”按钮,头部按当前语言渲染动态链接,新窗口加 rel=noopener noreferrer
- 后台管理页新增“头部导航链接”卡片(三语文案、URL、打开方式、排序、状态),改动即时生效
- 补充 nav 接口与迁移测试、三语文案,重新生成 Swagger 文档
2026-09-21 21:26:30 +08:00

172 lines
4.2 KiB
Go

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
}