219 lines
6.2 KiB
Go
219 lines
6.2 KiB
Go
// 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)
|
||
}
|