feat: PhotoWall 毕业照存储系统初始版本

后端: Go(Gin+GORM+MySQL+Redis)
- 17张业务表(yz_pw_前缀), 自动迁移
- JWT认证(3小时) + 盐+MD5密码 + 图形验证码
- 班级CRUD/加入(8人姓名验证/邀请码)/审核/30天自动清理
- 系统配置(16项)/菜单管理(动态路由)/数据统计
- 文件MD5去重/数据隔离/账号封禁/敏感词DFA检测

前端: Vue3+Vite+Element Plus+Less+ECharts+FontAwesome
- 动态路由(数据库菜单驱动)
- 蓝白配色, H5响应式
- 登录/注册/忘记密码/个人中心
- 班级创建向导/详情/加入/列表
- 管理后台: 审核/配置/菜单/统计/用户/敏感词

数据库: MySQL 10.31.100.3:3306/photowall
管理员: hero920103 / 920103
This commit is contained in:
hero920103
2026-09-03 05:55:23 +08:00
commit 57d9866dab
82 changed files with 12668 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
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
// 数据迁移:旧版 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)
}
// 创建平台管理员
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.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
}