Files
SproutGate/sproutgate-backend/cmd/migrate/main.go

219 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// migrate 将旧版 JSON 文件数据迁移到 MySQL 数据库。
//
// 用法:
//
// go run ./cmd/migrate [--data-dir ./data]
//
// 环境变量(与主程序相同):
//
// APP_ENV=production 使用生产数据库(默认使用开发测试库)
// DB_DSN 自定义 DSN优先级最高
package main
import (
"encoding/base64"
"encoding/json"
"flag"
"log"
"os"
"path/filepath"
"strings"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"sproutgate-backend/internal/database"
"sproutgate-backend/internal/models"
"sproutgate-backend/internal/storage"
)
func main() {
dataDir := flag.String("data-dir", "./data", "旧版 JSON 数据目录路径")
flag.Parse()
absDir, err := filepath.Abs(*dataDir)
if err != nil {
log.Fatalf("路径解析失败: %v", err)
}
log.Printf("数据目录: %s", absDir)
db, err := database.Open()
if err != nil {
log.Fatalf("连接数据库失败: %v", err)
}
// 自动迁移表结构
if err := db.AutoMigrate(
&storage.DBUser{},
&storage.DBPendingUser{},
&storage.DBResetPassword{},
&storage.DBSecondaryEmailVerification{},
&storage.DBAppConfig{},
&storage.DBInviteCode{},
); err != nil {
log.Fatalf("表结构同步失败: %v", err)
}
log.Println("✓ 表结构已同步")
migrateAdminConfig(db, absDir)
migrateAuthConfig(db, absDir)
migrateEmailConfig(db, absDir)
migrateCheckinConfig(db, absDir)
migrateRegistrationConfig(db, absDir)
migrateUsers(db, absDir)
log.Println("\n✅ 数据迁移完成!")
}
// ── 辅助函数 ──────────────────────────────────────────────────────────────────
func readJSONFile(path string, target any) error {
raw, err := os.ReadFile(path)
if err != nil {
return err
}
return json.Unmarshal(raw, target)
}
// putConfig 将任意值序列化为 JSON 后写入 app_configs冲突时整体覆盖
func putConfig(db *gorm.DB, key string, value any) {
raw, err := json.Marshal(value)
if err != nil {
log.Printf(" ✗ 序列化 [%s] 失败: %v", key, err)
return
}
row := storage.DBAppConfig{ConfigKey: key, ConfigValue: string(raw)}
if err := db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&row).Error; err != nil {
log.Printf(" ✗ app_configs[%s] 失败: %v", key, err)
} else {
log.Printf(" ✓ app_configs[%s]", key)
}
}
// ── 各节配置迁移 ──────────────────────────────────────────────────────────────
func migrateAdminConfig(db *gorm.DB, dataDir string) {
path := filepath.Join(dataDir, "config", "admin.json")
var cfg storage.AdminConfig
if err := readJSONFile(path, &cfg); err != nil {
log.Printf("[admin] 跳过(%v", err)
return
}
putConfig(db, "admin", cfg)
}
func migrateAuthConfig(db *gorm.DB, dataDir string) {
path := filepath.Join(dataDir, "config", "auth.json")
var cfg storage.AuthConfig
if err := readJSONFile(path, &cfg); err != nil {
log.Printf("[auth] 跳过(%v", err)
return
}
putConfig(db, "auth", cfg)
}
func migrateEmailConfig(db *gorm.DB, dataDir string) {
path := filepath.Join(dataDir, "config", "email.json")
var cfg storage.EmailConfig
if err := readJSONFile(path, &cfg); err != nil {
log.Printf("[email] 跳过(%v", err)
return
}
putConfig(db, "email", cfg)
}
func migrateCheckinConfig(db *gorm.DB, dataDir string) {
path := filepath.Join(dataDir, "config", "checkin.json")
var cfg storage.CheckInConfig
if err := readJSONFile(path, &cfg); err != nil {
log.Printf("[checkin] 跳过(%v", err)
return
}
putConfig(db, "checkin", cfg)
}
type oldRegistrationConfig struct {
RequireInviteCode bool `json:"requireInviteCode"`
Invites []storage.InviteEntry `json:"invites"`
}
type registrationPolicy struct {
RequireInviteCode bool `json:"requireInviteCode"`
ForbiddenAccounts string `json:"forbiddenAccounts"`
InviteRegisterRewardCoins *int `json:"inviteRegisterRewardCoins,omitempty"`
}
func migrateRegistrationConfig(db *gorm.DB, dataDir string) {
path := filepath.Join(dataDir, "config", "registration.json")
var old oldRegistrationConfig
if err := readJSONFile(path, &old); err != nil {
log.Printf("[registration] 跳过(%v", err)
return
}
putConfig(db, "registration", registrationPolicy{RequireInviteCode: old.RequireInviteCode})
for _, entry := range old.Invites {
row := storage.DBInviteCode{
Code: entry.Code,
Note: entry.Note,
MaxUses: entry.MaxUses,
Uses: entry.Uses,
ExpiresAt: entry.ExpiresAt,
CreatedAt: entry.CreatedAt,
}
if err := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&row).Error; err != nil {
log.Printf(" ✗ 邀请码 %s 失败: %v", entry.Code, err)
} else {
log.Printf(" ✓ 邀请码 %s", entry.Code)
}
}
}
func migrateUsers(db *gorm.DB, dataDir string) {
usersDir := filepath.Join(dataDir, "users")
entries, err := os.ReadDir(usersDir)
if err != nil {
log.Printf("[users] 读取目录失败: %v", err)
return
}
total, ok, skipped := 0, 0, 0
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
total++
path := filepath.Join(usersDir, entry.Name())
var record models.UserRecord
if err := readJSONFile(path, &record); err != nil {
log.Printf(" ✗ 读取 %s 失败: %v", entry.Name(), err)
skipped++
continue
}
// 优先用 JSON 中的 account 字段没有则从文件名base64解码
if strings.TrimSpace(record.Account) == "" {
name := strings.TrimSuffix(entry.Name(), ".json")
decoded, decErr := base64.RawURLEncoding.DecodeString(name)
if decErr != nil || len(decoded) == 0 {
log.Printf(" ✗ 无法确定账号,跳过 %s", entry.Name())
skipped++
continue
}
record.Account = string(decoded)
}
row := storage.DBUserFromRecord(record)
if err := db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&row).Error; err != nil {
log.Printf(" ✗ 用户 %s 失败: %v", record.Account, err)
skipped++
} else {
log.Printf(" ✓ 用户 %s", record.Account)
ok++
}
}
log.Printf("[users] 合计 %d 个,成功 %d跳过 %d", total, ok, skipped)
}