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:
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"photowall/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// seedRegions 写入示例行政区划数据。
|
||||
// 全球国家/省/区县数据量巨大,此处仅写入代表性示例,平台管理员可通过
|
||||
// 管理接口批量录入完整数据(或导入民政部/GeoNames 数据源)。
|
||||
func seedRegions(db *gorm.DB) error {
|
||||
// 国家
|
||||
china := upsertRegion(db, model.Region{Name: "中国", NameEn: "China", Level: model.LevelCountry, Code: "CN", SortOrder: 1})
|
||||
usa := upsertRegion(db, model.Region{Name: "美国", NameEn: "United States", Level: model.LevelCountry, Code: "US", SortOrder: 2})
|
||||
uk := upsertRegion(db, model.Region{Name: "英国", NameEn: "United Kingdom", Level: model.LevelCountry, Code: "GB", SortOrder: 3})
|
||||
japan := upsertRegion(db, model.Region{Name: "日本", NameEn: "Japan", Level: model.LevelCountry, Code: "JP", SortOrder: 4})
|
||||
|
||||
// 中国 - 省/直辖市
|
||||
jiangsu := upsertRegion(db, model.Region{Name: "江苏省", NameEn: "Jiangsu", Level: model.LevelProvince, ParentID: china.ID, Code: "320000"})
|
||||
beijing := upsertRegion(db, model.Region{Name: "北京市", NameEn: "Beijing", Level: model.LevelProvince, ParentID: china.ID, Code: "110000"})
|
||||
guangdong := upsertRegion(db, model.Region{Name: "广东省", NameEn: "Guangdong", Level: model.LevelProvince, ParentID: china.ID, Code: "440000"})
|
||||
shanghai := upsertRegion(db, model.Region{Name: "上海市", NameEn: "Shanghai", Level: model.LevelProvince, ParentID: china.ID, Code: "310000"})
|
||||
zhejiang := upsertRegion(db, model.Region{Name: "浙江省", NameEn: "Zhejiang", Level: model.LevelProvince, ParentID: china.ID, Code: "330000"})
|
||||
|
||||
// 中国 - 区县(最低级)
|
||||
upsertRegion(db, model.Region{Name: "海州区", NameEn: "Haizhou", Level: model.LevelCounty, ParentID: jiangsu.ID, Code: "320706"})
|
||||
upsertRegion(db, model.Region{Name: "连云区", NameEn: "Lianyun", Level: model.LevelCounty, ParentID: jiangsu.ID, Code: "320703"})
|
||||
upsertRegion(db, model.Region{Name: "赣榆区", NameEn: "Ganyu", Level: model.LevelCounty, ParentID: jiangsu.ID, Code: "320707"})
|
||||
upsertRegion(db, model.Region{Name: "玄武区", NameEn: "Xuanwu", Level: model.LevelCounty, ParentID: jiangsu.ID, Code: "320102"})
|
||||
upsertRegion(db, model.Region{Name: "鼓楼区", NameEn: "Gulou", Level: model.LevelCounty, ParentID: jiangsu.ID, Code: "320106"})
|
||||
upsertRegion(db, model.Region{Name: "海淀区", NameEn: "Haidian", Level: model.LevelCounty, ParentID: beijing.ID, Code: "110108"})
|
||||
upsertRegion(db, model.Region{Name: "朝阳区", NameEn: "Chaoyang", Level: model.LevelCounty, ParentID: beijing.ID, Code: "110105"})
|
||||
upsertRegion(db, model.Region{Name: "西城区", NameEn: "Xicheng", Level: model.LevelCounty, ParentID: beijing.ID, Code: "110102"})
|
||||
upsertRegion(db, model.Region{Name: "天河区", NameEn: "Tianhe", Level: model.LevelCounty, ParentID: guangdong.ID, Code: "440106"})
|
||||
upsertRegion(db, model.Region{Name: "南山区", NameEn: "Nanshan", Level: model.LevelCounty, ParentID: guangdong.ID, Code: "440305"})
|
||||
upsertRegion(db, model.Region{Name: "福田区", NameEn: "Futian", Level: model.LevelCounty, ParentID: guangdong.ID, Code: "440304"})
|
||||
upsertRegion(db, model.Region{Name: "浦东新区", NameEn: "Pudong", Level: model.LevelCounty, ParentID: shanghai.ID, Code: "310115"})
|
||||
upsertRegion(db, model.Region{Name: "黄浦区", NameEn: "Huangpu", Level: model.LevelCounty, ParentID: shanghai.ID, Code: "310101"})
|
||||
upsertRegion(db, model.Region{Name: "西湖区", NameEn: "Xihu", Level: model.LevelCounty, ParentID: zhejiang.ID, Code: "330106"})
|
||||
upsertRegion(db, model.Region{Name: "滨江区", NameEn: "Binjiang", Level: model.LevelCounty, ParentID: zhejiang.ID, Code: "330108"})
|
||||
|
||||
// 美国 - 州
|
||||
california := upsertRegion(db, model.Region{Name: "加利福尼亚州", NameEn: "California", Level: model.LevelProvince, ParentID: usa.ID, Code: "CA"})
|
||||
newyorkState := upsertRegion(db, model.Region{Name: "纽约州", NameEn: "New York", Level: model.LevelProvince, ParentID: usa.ID, Code: "NY"})
|
||||
// 美国 - 县
|
||||
upsertRegion(db, model.Region{Name: "洛杉矶县", NameEn: "Los Angeles County", Level: model.LevelCounty, ParentID: california.ID, Code: "06037"})
|
||||
upsertRegion(db, model.Region{Name: "旧金山县", NameEn: "San Francisco County", Level: model.LevelCounty, ParentID: california.ID, Code: "06075"})
|
||||
upsertRegion(db, model.Region{Name: "圣克拉拉县", NameEn: "Santa Clara County", Level: model.LevelCounty, ParentID: california.ID, Code: "06085"})
|
||||
upsertRegion(db, model.Region{Name: "纽约县", NameEn: "New York County", Level: model.LevelCounty, ParentID: newyorkState.ID, Code: "36061"})
|
||||
|
||||
// 英国
|
||||
england := upsertRegion(db, model.Region{Name: "英格兰", NameEn: "England", Level: model.LevelProvince, ParentID: uk.ID, Code: "ENG"})
|
||||
upsertRegion(db, model.Region{Name: "大伦敦", NameEn: "Greater London", Level: model.LevelCounty, ParentID: england.ID, Code: "LND"})
|
||||
upsertRegion(db, model.Region{Name: "曼彻斯特", NameEn: "Manchester", Level: model.LevelCounty, ParentID: england.ID, Code: "MAN"})
|
||||
|
||||
// 日本
|
||||
tokyo := upsertRegion(db, model.Region{Name: "东京都", NameEn: "Tokyo", Level: model.LevelProvince, ParentID: japan.ID, Code: "13"})
|
||||
upsertRegion(db, model.Region{Name: "东京都区部", NameEn: "Tokyo Metropolis", Level: model.LevelCounty, ParentID: tokyo.ID, Code: "13100"})
|
||||
upsertRegion(db, model.Region{Name: "八王子市", NameEn: "Hachioji", Level: model.LevelCounty, ParentID: tokyo.ID, Code: "13201"})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// upsertRegion 按 name+level+parent_id 查找,不存在则创建。
|
||||
func upsertRegion(db *gorm.DB, r model.Region) model.Region {
|
||||
var existing model.Region
|
||||
err := db.Where("name = ? AND level = ? AND parent_id = ?", r.Name, r.Level, r.ParentID).First(&existing).Error
|
||||
if err == nil {
|
||||
return existing
|
||||
}
|
||||
if err := db.Create(&r).Error; err != nil {
|
||||
// 并发或重复创建时返回已存在记录
|
||||
db.Where("name = ? AND level = ? AND parent_id = ?", r.Name, r.Level, r.ParentID).First(&existing)
|
||||
return existing
|
||||
}
|
||||
return r
|
||||
}
|
||||
Reference in New Issue
Block a user