151 lines
3.8 KiB
Go
151 lines
3.8 KiB
Go
package database
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"log"
|
||
"time"
|
||
|
||
"photowall/internal/config"
|
||
"photowall/internal/model"
|
||
"photowall/pkg/hash"
|
||
|
||
"gorm.io/driver/mysql"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/logger"
|
||
"gorm.io/gorm/schema"
|
||
)
|
||
|
||
var DB *gorm.DB
|
||
|
||
// Init 连接 MySQL,自动建表,创建平台管理员,写入种子数据。
|
||
func Init() error {
|
||
cfg := config.C
|
||
|
||
dsn := cfg.DSN()
|
||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||
Logger: logger.Default.LogMode(logger.Warn),
|
||
NamingStrategy: schema.NamingStrategy{
|
||
TablePrefix: "yz_pw_", // 统一表前缀
|
||
SingularTable: false,
|
||
},
|
||
NowFunc: func() time.Time { return time.Now().Local() },
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("connect mysql: %w", err)
|
||
}
|
||
|
||
sqlDB, err := db.DB()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
sqlDB.SetMaxOpenConns(50)
|
||
sqlDB.SetMaxIdleConns(10)
|
||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||
|
||
DB = db
|
||
|
||
if !cfg.DBAutoMigrate {
|
||
log.Println("[database] mysql connected (auto migrate skipped)")
|
||
return nil
|
||
}
|
||
|
||
log.Println("[database] running auto migration and seeding...")
|
||
|
||
// 增量保护:确保花名册新表及 row_count 字段迁移
|
||
_ = db.AutoMigrate(&model.ClassRoster{})
|
||
if !db.Migrator().HasColumn(&model.Class{}, "row_count") {
|
||
_ = db.Migrator().AddColumn(&model.Class{}, "row_count")
|
||
}
|
||
|
||
// 数据迁移:旧版 bcrypt 密码(60字符) 超过新版 md5(32字符) 列宽,
|
||
// 在 AutoMigrate 缩列前先清空旧密码,后续 ensureAdmin 会重新设置。
|
||
// 注意:salt 列此时可能还不存在,只更新 password_hash。
|
||
db.Exec("UPDATE yz_pw_users SET password_hash = '' WHERE LENGTH(password_hash) > 32")
|
||
|
||
// 自动迁移
|
||
if err := autoMigrate(db); err != nil {
|
||
return fmt.Errorf("auto migrate: %w", err)
|
||
}
|
||
|
||
// 历史数据回填:school_types 多学段字段由原单一 school_type 初始化
|
||
db.Exec("UPDATE yz_pw_schools SET school_types = school_type WHERE (school_types IS NULL OR school_types = '') AND school_type <> ''")
|
||
|
||
// 创建平台管理员
|
||
if err := ensureAdmin(db); err != nil {
|
||
return fmt.Errorf("ensure admin: %w", err)
|
||
}
|
||
|
||
// 写入种子地区数据
|
||
if err := seedRegions(db); err != nil {
|
||
return fmt.Errorf("seed regions: %w", err)
|
||
}
|
||
|
||
log.Println("[database] mysql connected, tables migrated, admin & seeds ready")
|
||
return nil
|
||
}
|
||
|
||
func autoMigrate(db *gorm.DB) error {
|
||
return db.AutoMigrate(
|
||
&model.User{},
|
||
&model.UserContact{},
|
||
&model.EducationHistory{},
|
||
&model.Region{},
|
||
&model.School{},
|
||
&model.College{},
|
||
&model.Class{},
|
||
&model.ClassMember{},
|
||
&model.ClassRoster{},
|
||
&model.ClassPhoto{},
|
||
&model.Invitation{},
|
||
&model.AuditLog{},
|
||
&model.SmsCode{},
|
||
&model.PasswordReset{},
|
||
&model.SystemConfig{},
|
||
&model.Menu{},
|
||
&model.UploadFile{},
|
||
&model.SensitiveWord{},
|
||
)
|
||
}
|
||
|
||
func ensureAdmin(db *gorm.DB) error {
|
||
cfg := config.C
|
||
var admin model.User
|
||
err := db.Where("role = ?", "admin").First(&admin).Error
|
||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return err
|
||
}
|
||
|
||
salt, pwdHash := hash.Password(cfg.AdminPassword)
|
||
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
// 不存在则创建
|
||
admin = model.User{
|
||
Username: cfg.AdminUsername,
|
||
PasswordHash: pwdHash,
|
||
Salt: salt,
|
||
Nickname: "平台管理员",
|
||
Role: "admin",
|
||
Bio: "系统默认平台管理员,负责班级审核与巡查",
|
||
}
|
||
return db.Create(&admin).Error
|
||
}
|
||
|
||
// 已存在:用户名/密码方式变更时同步更新(兼容旧 bcrypt 数据)
|
||
needUpdate := false
|
||
if admin.Username != cfg.AdminUsername {
|
||
admin.Username = cfg.AdminUsername
|
||
needUpdate = true
|
||
}
|
||
// bcrypt 哈希长度 60,md5 长度 32;salt 为空说明是旧数据
|
||
if len(admin.PasswordHash) == 60 || admin.Salt == "" {
|
||
admin.PasswordHash = pwdHash
|
||
admin.Salt = salt
|
||
needUpdate = true
|
||
}
|
||
if needUpdate {
|
||
return db.Save(&admin).Error
|
||
}
|
||
return nil
|
||
}
|