feat: 更新SproutGate前后端代码

This commit is contained in:
2026-04-01 22:04:01 +08:00
parent 90590c7cb0
commit 650e1c7707
49 changed files with 3609 additions and 768 deletions

View File

@@ -0,0 +1,218 @@
// 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)
}