Files
rill/internal/database/seed.go
T
kevin 2858d3c0f7 接口响应与文档文案全部改为英文
- 错误响应、健康检查等运行时文案改为英文
- 种子数据英文化,新增迁移 v5 更新存量内置数据(不覆盖手工修改)
- Swagger 注释与 docs/ 文档全英文化
- 补充英文文案断言与 v5 迁移测试
2026-09-20 02:13:17 +08:00

120 lines
3.3 KiB
Go

package database
import (
"crypto/rand"
"fmt"
"log/slog"
"math/big"
"os"
"path/filepath"
"strings"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"rill/internal/model"
)
const (
adminUsername = "admin"
adminEmail = "admin@example.com"
adminPasswordLen = 16
adminPasswordFilename = "admin_password.txt"
)
// adminPasswordCharset 去掉了易混淆字符(0/O、1/l/I)。
const adminPasswordCharset = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
// seedAdminUser 创建初始管理员用户并加入 admin 组,密码随机生成且仅在迁移时打印一次。
func seedAdminUser(tx *gorm.DB) error {
var count int64
if err := tx.Model(&model.User{}).Where("username = ?", adminUsername).Count(&count).Error; err != nil {
return err
}
if count > 0 {
slog.Warn("已存在同名用户,跳过初始管理员创建", "username", adminUsername)
return nil
}
password, err := generatePassword(adminPasswordLen)
if err != nil {
return err
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
user := model.User{
Username: adminUsername,
Email: adminEmail,
PasswordHash: string(hash),
Nickname: "Administrator",
Status: 1,
}
if err := tx.Create(&user).Error; err != nil {
return err
}
if err := tx.Exec(
"INSERT INTO user_group_members (user_id, user_group_id) VALUES (?, ?)",
user.ID, model.GroupIDAdmin,
).Error; err != nil {
return err
}
passwordFile := adminPasswordPath(tx)
if err := writeAdminPasswordFile(passwordFile, password); err != nil {
slog.Warn("初始管理员密码文件保存失败", "path", passwordFile, "err", err)
}
fmt.Printf(
"\n初始管理员账号已创建,密码仅显示这一次,请立即保存:\n 用户名: %s\n 密码: %s\n 密码文件: %s(登录后请立即删除该文件)\n\n",
adminUsername, password, passwordFile,
)
return nil
}
// adminPasswordPath 密码文件与 SQLite 数据库同目录;其他驱动回落到 ./data。
func adminPasswordPath(db *gorm.DB) string {
if db.Dialector.Name() == "sqlite" {
if file := sqliteDatabaseFile(db); file != "" && file != ":memory:" {
return filepath.Join(filepath.Dir(file), adminPasswordFilename)
}
}
return filepath.Join("data", adminPasswordFilename)
}
func sqliteDatabaseFile(db *gorm.DB) string {
var rows []struct {
File string
}
if err := db.Raw("PRAGMA database_list").Scan(&rows).Error; err != nil || len(rows) == 0 {
return ""
}
return rows[0].File
}
func writeAdminPasswordFile(path, password string) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
content := fmt.Sprintf(
"初始管理员账号(密码仅明文保存在此,登录后请立即删除本文件)\n\n用户名: %s\n密码: %s\n",
adminUsername, password,
)
return os.WriteFile(path, []byte(content), 0o600)
}
func generatePassword(length int) (string, error) {
limit := big.NewInt(int64(len(adminPasswordCharset)))
var builder strings.Builder
builder.Grow(length)
for i := 0; i < length; i++ {
n, err := rand.Int(rand.Reader, limit)
if err != nil {
return "", fmt.Errorf("生成随机密码失败: %w", err)
}
builder.WriteByte(adminPasswordCharset[n.Int64()])
}
return builder.String(), nil
}