feat: 实现完整的板块级RBAC内容审核系统
- 新增站长/超级管理员/管理员/板块管理员四级角色体系 - 实现实时权限快照加载与细粒度权限校验 - 新增内容审核队列与前后端页面 - 重构用户权限管理与站点管理逻辑 - 新增评论/帖子审核状态与通知推送 - 优化前端权限控制与角色徽章展示 - 修正数据库迁移与默认值问题
This commit is contained in:
@@ -31,11 +31,23 @@ func InitDB(dsn string) error {
|
||||
|
||||
if err := db.AutoMigrate(
|
||||
&User{}, &Board{}, &Post{}, &Comment{}, &RefreshToken{}, &Like{}, &Notification{}, &Checkin{},
|
||||
&Announcement{}, &SiteSetting{}, &Attachment{},
|
||||
&Announcement{}, &SiteSetting{}, &Attachment{}, &UserBoard{}, &LoginLog{},
|
||||
); err != nil {
|
||||
return fmt.Errorf("自动迁移失败: %w", err)
|
||||
}
|
||||
|
||||
// RBAC:把初始管理员(id 最小的 admin,通常即首个注册账号)升级为站长;
|
||||
// 已存在 owner 时不动数据,保证幂等
|
||||
if err := ensureOwnerRole(db); err != nil {
|
||||
return fmt.Errorf("站长角色迁移失败: %w", err)
|
||||
}
|
||||
|
||||
// login_logs.success 早期 default=true 与 GORM 零值省略叠加,
|
||||
// 会把失败登录错存为成功;AutoMigrate 不会改列默认值,这里幂等修正
|
||||
if err := db.Exec(`ALTER TABLE login_logs ALTER COLUMN success SET DEFAULT false`).Error; err != nil {
|
||||
return fmt.Errorf("login_logs 默认值修正失败: %w", err)
|
||||
}
|
||||
|
||||
// 新表结构就位后删除遗留的明文列
|
||||
if err := dropLegacyRefreshTokenColumn(db); err != nil {
|
||||
return fmt.Errorf("refresh token 旧列清理失败: %w", err)
|
||||
@@ -140,6 +152,31 @@ func dropLegacyRefreshTokenColumn(db *gorm.DB) error {
|
||||
return db.Exec(`ALTER TABLE refresh_tokens DROP COLUMN token`).Error
|
||||
}
|
||||
|
||||
// ensureOwnerRole 若无站长,则把 id 最小的旧管理员升级为站长;
|
||||
// 连管理员都没有的全新库,把 id=1 的初始账号设为站长
|
||||
func ensureOwnerRole(db *gorm.DB) error {
|
||||
var ownerCount int64
|
||||
if err := db.Model(&User{}).Where("role = ?", RoleOwner).Count(&ownerCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if ownerCount > 0 {
|
||||
return nil
|
||||
}
|
||||
res := db.Model(&User{}).Where("role = ?", RoleAdmin).
|
||||
Order("id ASC").Limit(1).Update("role", RoleOwner)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
if err := db.Model(&User{}).Order("id ASC").Limit(1).
|
||||
Update("role", RoleOwner).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Println("[model] 已迁移初始账号为站长角色(owner)")
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedDefaultBoards 写入默认板块
|
||||
func seedDefaultBoards(db *gorm.DB) {
|
||||
defaults := []Board{
|
||||
|
||||
@@ -6,14 +6,47 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Role 用户角色
|
||||
// Role 用户角色(等级递增:普通用户 < 板块管理员 < 管理员 < 超级管理员 < 站长)
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleUser Role = "user"
|
||||
RoleAdmin Role = "admin"
|
||||
RoleUser Role = "user" // 普通用户:发帖/评论需审核
|
||||
RoleBoardAdmin Role = "board_admin" // 板块管理员:仅管理被授权板块的帖子与评论
|
||||
RoleAdmin Role = "admin" // 管理员:公告 + 全站帖子/评论审核
|
||||
RoleSuperAdmin Role = "super_admin" // 超级管理员:全站后台(用户/公告/设置/内容),但不可操作站长
|
||||
RoleOwner Role = "owner" // 站长:最高权限,全站唯一,任何人(含自己)不可改角色/封禁
|
||||
)
|
||||
|
||||
// RoleLevel 角色等级,用于层级比较;未知角色按普通用户处理
|
||||
func RoleLevel(r Role) int {
|
||||
switch r {
|
||||
case RoleOwner:
|
||||
return 100
|
||||
case RoleSuperAdmin:
|
||||
return 80
|
||||
case RoleAdmin:
|
||||
return 50
|
||||
case RoleBoardAdmin:
|
||||
return 30
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// IsStaff 是否为任一管理角色(板块管理员及以上)
|
||||
func IsStaff(r Role) bool {
|
||||
return RoleLevel(r) >= RoleLevel(RoleBoardAdmin)
|
||||
}
|
||||
|
||||
// ValidRole 角色枚举校验
|
||||
func ValidRole(r Role) bool {
|
||||
switch r {
|
||||
case RoleUser, RoleBoardAdmin, RoleAdmin, RoleSuperAdmin, RoleOwner:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 内容审核状态
|
||||
const (
|
||||
ContentStatusPending = "pending"
|
||||
@@ -39,7 +72,7 @@ type User struct {
|
||||
Role Role `gorm:"size:16;default:user" json:"role"`
|
||||
Banned bool `gorm:"default:false" json:"banned"`
|
||||
TokenVersion int `gorm:"default:0" json:"-"` // token 版本号,改密码/封禁时递增使旧 JWT 失效
|
||||
LastSeenAt *time.Time `gorm:"index" json:"-"` // 最近活跃时间(在线统计,限频更新)
|
||||
LastSeenAt *time.Time `gorm:"index" json:"-"` // 最近活跃时间(在线统计,限频更新)
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
@@ -136,15 +169,17 @@ type Checkin struct {
|
||||
|
||||
// 通知类型
|
||||
const (
|
||||
NotificationTypeComment = "comment" // 评论了你的帖子
|
||||
NotificationTypeReply = "reply" // 回复了你的评论
|
||||
NotificationTypeLike = "like" // 点赞了你的帖子
|
||||
NotificationTypeComment = "comment" // 评论了你的帖子
|
||||
NotificationTypeReply = "reply" // 回复了你的评论
|
||||
NotificationTypeLike = "like" // 点赞了你的帖子
|
||||
NotificationTypeApproved = "approved" // 内容审核通过
|
||||
NotificationTypeRejected = "rejected" // 内容审核未通过
|
||||
)
|
||||
|
||||
// Notification 站内通知
|
||||
type Notification struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"index;not null" json:"user_id"` // 接收通知的用户
|
||||
UserID uint `gorm:"index;not null" json:"user_id"` // 接收通知的用户
|
||||
ActorID uint `gorm:"not null" json:"actor_id"` // 触发通知的用户
|
||||
Type string `gorm:"size:16;not null;index" json:"type"` // comment | like
|
||||
PostID uint `gorm:"index;not null" json:"post_id"` // 关联帖子
|
||||
@@ -164,7 +199,7 @@ type Announcement struct {
|
||||
Content string `gorm:"type:text;not null" json:"content"`
|
||||
Tag string `gorm:"size:32;not null;default:公告" json:"tag"`
|
||||
TagColor string `gorm:"size:16;not null;default:blue" json:"tag_color"` // blue/green/orange/red/purple/gray
|
||||
Published bool `gorm:"not null;index" json:"published"` // 显式写入 false;不可用 default:true,否则草稿零值会被 GORM 省略而落成已发布
|
||||
Published bool `gorm:"not null;index" json:"published"` // 显式写入 false;不可用 default:true,否则草稿零值会被 GORM 省略而落成已发布
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
@@ -195,3 +230,25 @@ type Attachment struct {
|
||||
Height int `gorm:"not null;default:0" json:"height"`
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||
}
|
||||
|
||||
// UserBoard 板块管理员的板块授权(多对多;仅 role=board_admin 的行生效)
|
||||
type UserBoard struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"uniqueIndex:idx_user_board;not null" json:"user_id"`
|
||||
BoardID uint `gorm:"uniqueIndex:idx_user_board;not null" json:"board_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
User User `gorm:"foreignKey:UserID" json:"-"`
|
||||
Board Board `gorm:"foreignKey:BoardID" json:"board,omitempty"`
|
||||
}
|
||||
|
||||
// LoginLog 登录历史(成功与失败都记录;失败时用户名可能不存在,UserID 为 0)
|
||||
type LoginLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"index;not null;default:0" json:"user_id"`
|
||||
Username string `gorm:"size:128;index;not null;default:''" json:"username"`
|
||||
IP string `gorm:"size:45;not null;default:''" json:"ip"` // IPv4/IPv6 最长 45 字符
|
||||
UserAgent string `gorm:"size:512;not null;default:''" json:"user_agent"`
|
||||
Success bool `gorm:"index;not null;default:false" json:"success"`
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user