feat: 实现完整的板块级RBAC内容审核系统
- 新增站长/超级管理员/管理员/板块管理员四级角色体系 - 实现实时权限快照加载与细粒度权限校验 - 新增内容审核队列与前后端页面 - 重构用户权限管理与站点管理逻辑 - 新增评论/帖子审核状态与通知推送 - 优化前端权限控制与角色徽章展示 - 修正数据库迁移与默认值问题
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/middleware"
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"github.com/freefire/jiang13-bbs/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -56,13 +58,20 @@ func (h *Handlers) PostDetail(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的帖子 ID"})
|
||||
return
|
||||
}
|
||||
post, err := h.Post.GetByID(uint(id))
|
||||
claims := middleware.CurrentUser(c)
|
||||
var viewerID uint
|
||||
var loadActor func() *service.Actor
|
||||
if claims != nil {
|
||||
viewerID = claims.ID
|
||||
loadActor = h.actorLoader(claims.ID) // 懒加载:仅非已发布帖才查 DB
|
||||
}
|
||||
post, err := h.Post.GetByIDForViewer(uint(id), viewerID, loadActor)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"})
|
||||
return
|
||||
}
|
||||
// 填充点赞状态(仅登录用户)
|
||||
if claims := middleware.CurrentUser(c); claims != nil {
|
||||
if claims != nil {
|
||||
post.Liked = h.Like.HasLiked(post.ID, claims.ID)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"post": post})
|
||||
@@ -89,7 +98,13 @@ func (h *Handlers) CreatePost(c *gin.Context) {
|
||||
if postType == "" {
|
||||
postType = "normal"
|
||||
}
|
||||
post, err := h.Post.Create(claims.ID, req.BoardID, req.Title, req.Content, req.Tags, postType)
|
||||
// 管理团队成员发帖免审直发;普通用户进入待审核队列。
|
||||
// 角色变更会强制 JWT 失效(token_version 递增),claims.Role 可视为实时值
|
||||
status := model.ContentStatusPending
|
||||
if model.IsStaff(model.Role(claims.Role)) {
|
||||
status = model.ContentStatusPublished
|
||||
}
|
||||
post, err := h.Post.Create(claims.ID, req.BoardID, req.Title, req.Content, req.Tags, postType, status)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -117,9 +132,10 @@ func (h *Handlers) UpdatePost(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
post, err := h.Post.Update(uint(id), claims.ID, claims.Role, req.Title, req.Content, req.Tags)
|
||||
actor := h.loadActor(claims.ID)
|
||||
post, err := h.Post.Update(actor, uint(id), claims.ID, req.Title, req.Content, req.Tags)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
respondPostModError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"post": post})
|
||||
@@ -133,18 +149,16 @@ func (h *Handlers) DeletePost(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的帖子 ID"})
|
||||
return
|
||||
}
|
||||
if err := h.Post.Delete(uint(id), claims.ID, claims.Role); err != nil {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
if err := h.Post.Delete(h.loadActor(claims.ID), uint(id), claims.ID); err != nil {
|
||||
respondPostModError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "已删除"})
|
||||
}
|
||||
|
||||
// TogglePin 切换帖子置顶(仅管理员)
|
||||
// TogglePin 切换帖子置顶(管理员及以上,板块管理员无此权限)
|
||||
func (h *Handlers) TogglePin(c *gin.Context) {
|
||||
claims := middleware.CurrentUser(c)
|
||||
if claims.Role != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "仅管理员可操作"})
|
||||
if !h.requireAdminOrAbove(c) {
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
@@ -160,11 +174,9 @@ func (h *Handlers) TogglePin(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"pinned": pinned})
|
||||
}
|
||||
|
||||
// ToggleRecommend 切换帖子推荐(仅管理员)
|
||||
// ToggleRecommend 切换帖子加精(管理员及以上,板块管理员无此权限)
|
||||
func (h *Handlers) ToggleRecommend(c *gin.Context) {
|
||||
claims := middleware.CurrentUser(c)
|
||||
if claims.Role != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "仅管理员可操作"})
|
||||
if !h.requireAdminOrAbove(c) {
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
@@ -179,3 +191,25 @@ func (h *Handlers) ToggleRecommend(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"recommended": recommended})
|
||||
}
|
||||
|
||||
// requireAdminOrAbove 置顶/加精仅管理员及以上可用;校验失败已写响应,返回 false
|
||||
func (h *Handlers) requireAdminOrAbove(c *gin.Context) bool {
|
||||
claims := middleware.CurrentUser(c)
|
||||
if model.RoleLevel(model.Role(claims.Role)) < model.RoleLevel(model.RoleAdmin) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "仅管理员可操作"})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// respondPostModError 帖子编辑/删除业务错误 → HTTP 状态码
|
||||
func respondPostModError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrPostNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
case errors.Is(err, service.ErrPostForbidden):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user