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
+24
View File
@@ -0,0 +1,24 @@
package model
import "time"
// SmsCode 短信验证码表
type SmsCode struct {
ID uint `gorm:"primaryKey" json:"id"`
Phone string `gorm:"size:20;index;not null" json:"phone"`
Code string `gorm:"size:8;not null" json:"-"`
Scene string `gorm:"size:32;not null" json:"scene"` // login / register / reset_password
Used bool `gorm:"default:false" json:"used"`
ExpireAt time.Time `json:"expire_at"`
CreatedAt time.Time `json:"created_at"`
}
// PasswordReset 密码重置码表
type PasswordReset struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
Code string `gorm:"size:8;index;not null" json:"-"`
Used bool `gorm:"default:false" json:"used"`
ExpireAt time.Time `json:"expire_at"`
CreatedAt time.Time `json:"created_at"`
}
+116
View File
@@ -0,0 +1,116 @@
package model
import (
"encoding/json"
"time"
)
// ClassStatus 班级审核状态
type ClassStatus string
const (
ClassPending ClassStatus = "pending" // 待审核(名字不足或等待平台审核)
ClassActive ClassStatus = "active" // 正常
ClassRejected ClassStatus = "rejected" // 被打回,需补齐
ClassDeleted ClassStatus = "deleted" // 已删除(30天未补齐自动删除或手动删除)
)
// Class 班级
// 唯一性:同一学校+学院+年级+班级序号+毕业年份 唯一
type Class struct {
ID uint `gorm:"primaryKey" json:"id"`
SchoolID uint `gorm:"index:idx_class_unique,priority:1;not null" json:"school_id"`
CollegeID uint `gorm:"index:idx_class_unique,priority:2" json:"college_id"`
Name string `gorm:"size:128;not null" json:"name"` // 班级展示名,如"计算机科学与技术2018级1班"
Grade string `gorm:"size:32;index:idx_class_unique,priority:3" json:"grade"` // 年级,如"2018级"
ClassNo string `gorm:"size:32;index:idx_class_unique,priority:4" json:"class_no"` // 班级序号,如"1班"/"3班"
GraduateYear int `gorm:"index:idx_class_unique,priority:5" json:"graduate_year"` // 毕业年份
AdminUserID uint `gorm:"index;not null" json:"admin_user_id"`
GraduationPhoto string `gorm:"size:512" json:"graduation_photo"` // 毕业照URL(每个班级唯一一张)
StudentNamesJSON string `gorm:"type:text" json:"-"` // 班级所有同学名字 JSON 数组
TeacherNamesJSON string `gorm:"type:text" json:"-"` // 老师名字 JSON 数组
Status ClassStatus `gorm:"size:16;default:pending;index" json:"status"`
RejectReason string `gorm:"size:512" json:"reject_reason"`
AuditDeadline time.Time `json:"audit_deadline"` // 补齐截止时间(创建+30天)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// StudentNames 读写同学名单
func (c *Class) StudentNames() []string {
var list []string
if c.StudentNamesJSON == "" {
return list
}
_ = json.Unmarshal([]byte(c.StudentNamesJSON), &list)
return list
}
func (c *Class) SetStudentNames(names []string) {
if names == nil {
names = []string{}
}
b, _ := json.Marshal(names)
c.StudentNamesJSON = string(b)
}
// TeacherNames 读写老师名单
func (c *Class) TeacherNames() []string {
var list []string
if c.TeacherNamesJSON == "" {
return list
}
_ = json.Unmarshal([]byte(c.TeacherNamesJSON), &list)
return list
}
func (c *Class) SetTeacherNames(names []string) {
if names == nil {
names = []string{}
}
b, _ := json.Marshal(names)
c.TeacherNamesJSON = string(b)
}
// MinStudentCount 加入班级验证所需最少同学数,也是创建班级录入名单的下限
const MinStudentCount = 8
// ClassMemberRole 班级成员角色
type ClassMemberRole string
const (
MemberRoleAdmin ClassMemberRole = "admin"
MemberRoleMember ClassMemberRole = "member"
)
// JoinMethod 加入方式
type JoinMethod string
const (
JoinByInvite JoinMethod = "invite" // 管理员邀请/邀请码
JoinBySearch JoinMethod = "search" // 搜索+8人验证
)
// ClassMember 班级成员
type ClassMember struct {
ID uint `gorm:"primaryKey" json:"id"`
ClassID uint `gorm:"uniqueIndex:idx_class_user,priority:1;not null" json:"class_id"`
UserID uint `gorm:"uniqueIndex:idx_class_user,priority:2;not null" json:"user_id"`
Role ClassMemberRole `gorm:"size:16;default:member;index" json:"role"`
JoinMethod JoinMethod `gorm:"size:16" json:"join_method"`
JoinedAt time.Time `json:"joined_at"`
CreatedAt time.Time `json:"created_at"`
}
// Invitation 班级邀请码
type Invitation struct {
ID uint `gorm:"primaryKey" json:"id"`
ClassID uint `gorm:"index;not null" json:"class_id"`
InviterID uint `gorm:"not null" json:"inviter_id"`
Code string `gorm:"size:32;uniqueIndex;not null" json:"code"`
MaxUses int `gorm:"default:50" json:"max_uses"`
UsedCount int `gorm:"default:0" json:"used_count"`
ExpiresAt time.Time `json:"expires_at"`
Status string `gorm:"size:16;default:active" json:"status"` // active / disabled
CreatedAt time.Time `json:"created_at"`
}
+18
View File
@@ -0,0 +1,18 @@
package model
import "time"
// SystemConfig 系统配置表(key-value 存储)
type SystemConfig struct {
ID uint `gorm:"primaryKey" json:"id"`
Key string `gorm:"size:64;uniqueIndex;not null" json:"key"`
Value string `gorm:"size:1024" json:"value"`
Label string `gorm:"size:128" json:"label"` // 显示名称
Description string `gorm:"size:512" json:"description"` // 参数说明
Category string `gorm:"size:32;index" json:"category"` // 分类:basic/geetest/sms/wechat/storage
Placeholder string `gorm:"size:256" json:"placeholder"` // 输入框示例
DocLink string `gorm:"size:256" json:"doc_link"` // 官方文档/获取地址
Sort int `gorm:"default:0" json:"sort"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+28
View File
@@ -0,0 +1,28 @@
package model
import "time"
// Menu 菜单/路由表(动态路由数据源)
type Menu struct {
ID uint `gorm:"primaryKey" json:"id"`
ParentID uint `gorm:"default:0;index" json:"parent_id"`
Name string `gorm:"size:64;uniqueIndex;not null" json:"name"` // 路由名称(唯一)
Path string `gorm:"size:255;not null" json:"path"` // 路由路径,如 /class/list
Component string `gorm:"size:255" json:"component"` // 组件相对路径,如 class/list/index(空则为目录)
Title string `gorm:"size:64;not null" json:"title"` // 菜单标题
Icon string `gorm:"size:64" json:"icon"` // fontawesome 图标类名,如 fas fa-home
Sort int `gorm:"default:0" json:"sort"` // 排序(从小到大)
Visible bool `gorm:"default:true" json:"visible"` // 是否在菜单中显示
RequireAuth bool `gorm:"default:true" json:"require_auth"` // 是否需要登录
RequireAdmin bool `gorm:"default:false" json:"require_admin"` // 是否需要管理员权限
KeepAlive bool `gorm:"default:false" json:"keep_alive"` // 是否缓存页面
Redirect string `gorm:"size:255" json:"redirect"` // 重定向路径(目录菜单用)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// MenuTree 菜单树结构(前端用)
type MenuTree struct {
Menu
Children []*MenuTree `json:"children"`
}
+37
View File
@@ -0,0 +1,37 @@
package model
import "time"
// MaxPhotosPerUserPerClass 每人在每个班级最多留存的当期照片数
const MaxPhotosPerUserPerClass = 10
// ClassPhoto 用户在班级下的个人当期照片(每人每班级最多10张)
type ClassPhoto struct {
ID uint `gorm:"primaryKey" json:"id"`
ClassID uint `gorm:"index;not null" json:"class_id"`
UserID uint `gorm:"index;not null" json:"user_id"`
PhotoURL string `gorm:"size:512;not null" json:"photo_url"`
Caption string `gorm:"size:255" json:"caption"`
CreatedAt time.Time `json:"created_at"`
}
// AuditAction 审核动作
type AuditAction string
const (
AuditSubmit AuditAction = "submit" // 提交审核
AuditApprove AuditAction = "approve" // 平台审核通过
AuditReject AuditAction = "reject" // 平台打回
AuditAutoDelete AuditAction = "auto_delete" // 30天未补齐自动删除
AuditManualDelete AuditAction = "manual_delete"
)
// AuditLog 班级审核日志
type AuditLog struct {
ID uint `gorm:"primaryKey" json:"id"`
ClassID uint `gorm:"index;not null" json:"class_id"`
AuditorID uint `json:"auditor_id"` // 平台管理员ID,系统自动操作为0
Action AuditAction `gorm:"size:24;index;not null" json:"action"`
Reason string `gorm:"size:512" json:"reason"`
CreatedAt time.Time `json:"created_at"`
}
+62
View File
@@ -0,0 +1,62 @@
package model
import "time"
// RegionLevel 地区层级
type RegionLevel string
const (
LevelCountry RegionLevel = "country"
LevelProvince RegionLevel = "province" // 省/州/直辖市
LevelCounty RegionLevel = "county" // 县/区(最低级)
)
// Region 地区表,自关联三级:国家 -> 省/州 -> 县/区
type Region struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:128;not null;index" json:"name"`
NameEn string `gorm:"size:128" json:"name_en"`
Level RegionLevel `gorm:"size:16;index;not null" json:"level"`
ParentID uint `gorm:"index" json:"parent_id"`
Code string `gorm:"size:32;index" json:"code"` // 行政区划代码/国家代码,可选
SortOrder int `gorm:"default:0" json:"sort_order"`
CreatedAt time.Time `json:"created_at"`
}
// SchoolType 学校类型
type SchoolType string
const (
SchoolUniversity SchoolType = "university" // 大学(含学院层级)
SchoolJuniorHigh SchoolType = "junior_high" // 初中
SchoolSeniorHigh SchoolType = "senior_high" // 高中
SchoolPrimary SchoolType = "primary" // 小学
SchoolKindergarten SchoolType = "kindergarten" // 幼儿园
SchoolVocational SchoolType = "vocational" // 职业学校/中专(也可设学院)
SchoolOther SchoolType = "other"
)
// HasCollege 该类型学校是否有学院层级
func (s SchoolType) HasCollege() bool {
return s == SchoolUniversity || s == SchoolVocational
}
// School 学校
type School struct {
ID uint `gorm:"primaryKey" json:"id"`
RegionID uint `gorm:"index;not null" json:"region_id"` // 必须挂在 county 级地区下
Name string `gorm:"size:128;not null;index" json:"name"`
NameEn string `gorm:"size:128" json:"name_en"`
SchoolType SchoolType `gorm:"size:24;index;not null" json:"school_type"`
Address string `gorm:"size:255" json:"address"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// College 学院(仅大学/职业学校有)
type College struct {
ID uint `gorm:"primaryKey" json:"id"`
SchoolID uint `gorm:"index;not null" json:"school_id"`
Name string `gorm:"size:128;not null" json:"name"`
CreatedAt time.Time `json:"created_at"`
}
+13
View File
@@ -0,0 +1,13 @@
package model
import "time"
// SensitiveWord 敏感词表(全局屏蔽用)
type SensitiveWord struct {
ID uint `gorm:"primaryKey" json:"id"`
Word string `gorm:"size:128;uniqueIndex;not null" json:"word"`
Category string `gorm:"size:32;default:other" json:"category"` // politics/porn/violence/advertising/other
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+18
View File
@@ -0,0 +1,18 @@
package model
import "time"
// UploadFile 上传文件记录表(MD5去重用)
type UploadFile struct {
ID uint `gorm:"primaryKey" json:"id"`
Md5 string `gorm:"size:32;uniqueIndex;not null" json:"md5"`
FilePath string `gorm:"size:512;not null" json:"file_path"` // 相对路径,如 classes/20260101_1_abc.jpg
FileUrl string `gorm:"size:512;not null" json:"file_url"` // 可访问URL,如 /uploads/classes/xxx.jpg
FileName string `gorm:"size:255" json:"file_name"`
FileSize int64 `gorm:"default:0" json:"file_size"`
FileType string `gorm:"size:32" json:"file_type"` // class/photo/avatar
UploaderID uint `gorm:"index" json:"uploader_id"`
RefCount int `gorm:"default:1" json:"ref_count"` // 引用次数
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+73
View File
@@ -0,0 +1,73 @@
package model
import (
"time"
)
// User 用户表
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"`
PasswordHash string `gorm:"size:32;not null" json:"-"` // MD5 32字符
Salt string `gorm:"size:32;not null" json:"-"` // 密码盐
Email string `gorm:"size:128;index" json:"email"` // 用于找回密码
Nickname string `gorm:"size:64" json:"nickname"`
Avatar string `gorm:"size:512" json:"avatar"`
Bio string `gorm:"size:512" json:"bio"`
Role string `gorm:"size:16;default:user;index" json:"role"` // user / admin
Status string `gorm:"size:16;default:active;index" json:"status"` // active / banned
BanReason string `gorm:"size:255" json:"ban_reason"`
BannedAt *time.Time `json:"banned_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ContactType 联系方式类型
type ContactType string
const (
ContactEmail ContactType = "email"
ContactQQ ContactType = "qq"
ContactWeChat ContactType = "wechat"
ContactPhone ContactType = "phone"
ContactAddress ContactType = "address"
ContactWorkUnit ContactType = "workunit"
)
// ContactMaxCount 各类型联系方式上限
var ContactMaxCount = map[ContactType]int{
ContactEmail: 2,
ContactQQ: 5,
ContactWeChat: 2,
ContactPhone: 5,
ContactAddress: 1,
ContactWorkUnit: 1,
}
// UserContact 用户联系方式
type UserContact struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
Type ContactType `gorm:"size:16;index;not null" json:"type"`
Value string `gorm:"size:255;not null" json:"value"`
IsPrimary bool `gorm:"default:false" json:"is_primary"`
CreatedAt time.Time `json:"created_at"`
}
// EducationHistory 学习履历(用户必须至少有一条才能创建班级)
type EducationHistory struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
SchoolID uint `gorm:"index" json:"school_id,omitempty"` // 关联平台学校(可选)
CollegeID uint `gorm:"index" json:"college_id,omitempty"` // 关联学院(可选)
ClassID uint `gorm:"index" json:"class_id,omitempty"` // 关联班级(可选)
SchoolName string `gorm:"size:128;not null" json:"school_name"` // 学校名称(冗余,便于自由填写)
CollegeName string `gorm:"size:128" json:"college_name"`
Major string `gorm:"size:128" json:"major"`
Degree string `gorm:"size:64" json:"degree"` // 本科/硕士/博士/高中/初中/小学/幼儿园
StartYear int `json:"start_year"`
EndYear int `json:"end_year"`
Description string `gorm:"size:512" json:"description"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}