Files
Scoreboard/server/internal/store/migrate.go
T
AskaEth 7d5d94f815 feat: M0-M2 骨架落地(服务端/前端/宿主/数据库/积分引擎)
- server: Go 单二进制骨架,embed 内嵌前端,配置/迁移/HTTP 路由
- store: system.db + event.db 两套迁移,业务表 + 种子数据
- domain: 积分引擎(并列三口径/计分通道/流水/排名/破纪录)+ 68 个测试
- web: Vite + Vue 三入口骨架 + shared 层(类型/API/WebSocket 客户端)
- console: Electron 宿主骨架(选屏/防熄屏/进程守护/托盘)
2026-09-09 18:36:40 +08:00

106 lines
2.6 KiB
Go

package store
import (
"database/sql"
"fmt"
"io/fs"
"log/slog"
"sort"
"git.yeij.top/AskaEth/Scoreboard/server/migrations"
)
const createMigrationsTable = `CREATE TABLE IF NOT EXISTS schema_migrations (
name TEXT PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
)`
// Migrate 按文件名顺序迁移系统库(data/system.db)。
func (s *Store) Migrate() error {
fsys, err := fs.Sub(migrations.SystemFS, "system")
if err != nil {
return fmt.Errorf("定位系统库迁移目录: %w", err)
}
return migrate(s.db, fsys, "system")
}
// MigrateEvent 迁移赛事库(event.db)。赛事库由调用方自行打开,
// 本函数只负责执行 event/ 下的迁移,不持有连接。
func MigrateEvent(db *sql.DB) error {
fsys, err := fs.Sub(migrations.EventFS, "event")
if err != nil {
return fmt.Errorf("定位赛事库迁移目录: %w", err)
}
return migrate(db, fsys, "event")
}
// migrate 执行 fsys 中尚未应用的迁移,每个迁移在独立事务中完成。
func migrate(db *sql.DB, fsys fs.FS, label string) error {
if _, err := db.Exec(createMigrationsTable); err != nil {
return fmt.Errorf("创建 schema_migrations: %w", err)
}
applied, err := appliedMigrations(db)
if err != nil {
return err
}
names, err := fs.Glob(fsys, "*.sql")
if err != nil {
return err
}
sort.Strings(names)
for _, name := range names {
if applied[name] {
continue
}
body, err := fs.ReadFile(fsys, name)
if err != nil {
return fmt.Errorf("读取迁移 %s: %w", name, err)
}
if err := applyMigration(db, name, string(body)); err != nil {
return err
}
slog.Info("已应用迁移", "db", label, "name", name)
}
return nil
}
func appliedMigrations(db *sql.DB) (map[string]bool, error) {
rows, err := db.Query(`SELECT name FROM schema_migrations`)
if err != nil {
return nil, fmt.Errorf("查询迁移记录: %w", err)
}
defer func() { _ = rows.Close() }()
applied := make(map[string]bool)
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
applied[name] = true
}
if err := rows.Err(); err != nil {
return nil, err
}
return applied, nil
}
func applyMigration(db *sql.DB, name, body string) error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("开启事务: %w", err)
}
defer func() { _ = tx.Rollback() }()
if _, err := tx.Exec(body); err != nil {
return fmt.Errorf("执行迁移 %s: %w", name, err)
}
if _, err := tx.Exec(`INSERT INTO schema_migrations(name) VALUES (?)`, name); err != nil {
return fmt.Errorf("记录迁移 %s: %w", name, err)
}
return tx.Commit()
}