feat: 实现完整的板块级RBAC内容审核系统
- 新增站长/超级管理员/管理员/板块管理员四级角色体系 - 实现实时权限快照加载与细粒度权限校验 - 新增内容审核队列与前后端页面 - 重构用户权限管理与站点管理逻辑 - 新增评论/帖子审核状态与通知推送 - 优化前端权限控制与角色徽章展示 - 修正数据库迁移与默认值问题
This commit is contained in:
22
backend/handler/actor.go
Normal file
22
backend/handler/actor.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/freefire/jiang13-bbs/service"
|
||||
)
|
||||
|
||||
// loadActor 立即加载用户的实时权限快照;失败返回 nil(业务层按无权限处理)
|
||||
func (h *Handlers) loadActor(userID uint) *service.Actor {
|
||||
actor, err := h.Auth.LoadActor(userID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return actor
|
||||
}
|
||||
|
||||
// actorLoader 返回懒加载回调:仅当业务层确实需要实时权限快照
|
||||
// (访问待审内容等场景)时才查 DB
|
||||
func (h *Handlers) actorLoader(userID uint) func() *service.Actor {
|
||||
return func() *service.Actor {
|
||||
return h.loadActor(userID)
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,13 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/middleware"
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"github.com/freefire/jiang13-bbs/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ===== 用户管理(RequireAdmin 兜底,前端不做权限判定) =====
|
||||
// ===== 用户管理(RequireStaff + PermUsers 兜底,前端不做权限判定) =====
|
||||
|
||||
// AdminListUsers 用户分页列表(搜索/角色/状态筛选 + 全站汇总)
|
||||
func (h *Handlers) AdminListUsers(c *gin.Context) {
|
||||
@@ -39,27 +40,30 @@ func (h *Handlers) AdminListUsers(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// adminUserActionBody 角色/封禁变更的通用请求体
|
||||
type adminUserRoleBody struct {
|
||||
Role string `json:"role"`
|
||||
// adminStaffBody 角色与板块授权变更请求体
|
||||
type adminStaffBody struct {
|
||||
Role string `json:"role"`
|
||||
BoardIDs []uint `json:"board_ids"`
|
||||
}
|
||||
|
||||
type adminUserBanBody struct {
|
||||
Banned bool `json:"banned"`
|
||||
}
|
||||
|
||||
// AdminUpdateUserRole 设置用户角色(user / admin)
|
||||
// AdminUpdateUserRole 设置管理角色与板块授权
|
||||
func (h *Handlers) AdminUpdateUserRole(c *gin.Context) {
|
||||
id, ok := parseAdminUserID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var body adminUserRoleBody
|
||||
var body adminStaffBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
|
||||
return
|
||||
}
|
||||
user, err := h.AdminUser.SetRole(middleware.CurrentUser(c).ID, id, body.Role)
|
||||
user, err := h.AdminUser.SetStaff(
|
||||
middleware.CurrentActor(c), id, model.Role(body.Role), body.BoardIDs,
|
||||
)
|
||||
if err != nil {
|
||||
respondAdminUserError(c, err)
|
||||
return
|
||||
@@ -78,7 +82,7 @@ func (h *Handlers) AdminSetUserBan(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
|
||||
return
|
||||
}
|
||||
user, err := h.AdminUser.SetBanned(middleware.CurrentUser(c).ID, id, body.Banned)
|
||||
user, err := h.AdminUser.SetBanned(middleware.CurrentActor(c), id, body.Banned)
|
||||
if err != nil {
|
||||
respondAdminUserError(c, err)
|
||||
return
|
||||
@@ -86,6 +90,21 @@ func (h *Handlers) AdminSetUserBan(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"user": user})
|
||||
}
|
||||
|
||||
// AdminUserLoginLogs 某用户的登录历史(IP/UA/成败)
|
||||
func (h *Handlers) AdminUserLoginLogs(c *gin.Context) {
|
||||
id, ok := parseAdminUserID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
logs, total, err := h.AdminUser.ListLoginLogs(id, page, 15)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取登录历史失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"logs": logs, "total": total, "page": page})
|
||||
}
|
||||
|
||||
// parseAdminUserID 解析路径中的用户 ID,失败时直接写出 400
|
||||
func parseAdminUserID(c *gin.Context) (uint, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
@@ -101,9 +120,14 @@ func respondAdminUserError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
case errors.Is(err, service.ErrProtectedOwner):
|
||||
// 站长保护属于权限边界而非参数错误
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
case errors.Is(err, service.ErrAdminSelfAction),
|
||||
errors.Is(err, service.ErrLastAdmin),
|
||||
errors.Is(err, service.ErrInvalidUserRole):
|
||||
errors.Is(err, service.ErrInvalidRole),
|
||||
errors.Is(err, service.ErrCannotAssignRole),
|
||||
errors.Is(err, service.ErrBoardRequired),
|
||||
errors.Is(err, service.ErrBoardNotFound):
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "操作失败"})
|
||||
|
||||
@@ -17,10 +17,11 @@ var (
|
||||
)
|
||||
|
||||
// setAuthCookies 设置认证 cookie:
|
||||
// - access token(HttpOnly,15min)
|
||||
// - refresh token(HttpOnly,7天,Path=/,供 Next middleware 在页面/RSC
|
||||
// 请求中读取并静默轮转;仅 /api/auth/refresh 端点消费)
|
||||
// - CSRF token(JS 可读,7天,双提交校验)
|
||||
// - access token(HttpOnly,15min)
|
||||
// - refresh token(HttpOnly,7天,Path=/,供 Next middleware 在页面/RSC
|
||||
// 请求中读取并静默轮转;仅 /api/auth/refresh 端点消费)
|
||||
// - CSRF token(JS 可读,7天,双提交校验)
|
||||
//
|
||||
// SameSite=Lax:允许外站顶级链接进入时保留登录态(Strict 会导致从外站
|
||||
// 跳转进来的第一次请求丢 cookie,把已登录用户误判为游客);状态变更请求
|
||||
// 另有 CSRF 双提交 token 兜底。生产 HTTPS 下 cookie 名带 __Host- 前缀。
|
||||
@@ -127,6 +128,7 @@ func (h *Handlers) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
setAuthCookies(c, accessToken, refreshToken, !h.Cfg.DevMode)
|
||||
h.AdminUser.RecordLogin(loginUser.ID, req.Username, c.ClientIP(), c.Request.UserAgent(), true)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"user": gin.H{
|
||||
"id": loginUser.ID,
|
||||
@@ -147,6 +149,11 @@ func (h *Handlers) Login(c *gin.Context) {
|
||||
}
|
||||
accessToken, refreshToken, user, err := h.Auth.Login(req.Username, req.Password)
|
||||
if err != nil {
|
||||
// 登录失败也留痕:用户存在时带 user_id(封禁/错密),不存在时为 0
|
||||
h.AdminUser.RecordLogin(
|
||||
h.Auth.GetUserIDByUsername(req.Username), req.Username,
|
||||
c.ClientIP(), c.Request.UserAgent(), false,
|
||||
)
|
||||
// 封禁与凭据错误区分:前端可据此展示针对性提示
|
||||
if errors.Is(err, service.ErrAccountBanned) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error(), "code": "account_banned"})
|
||||
@@ -155,6 +162,8 @@ func (h *Handlers) Login(c *gin.Context) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// 登录成功留痕(IP/UA/时间)
|
||||
h.AdminUser.RecordLogin(user.ID, req.Username, c.ClientIP(), c.Request.UserAgent(), true)
|
||||
// dev 模式不设 Secure,生产环境需 HTTPS
|
||||
setAuthCookies(c, accessToken, refreshToken, !h.Cfg.DevMode)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -232,8 +241,12 @@ func (h *Handlers) ChangePassword(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "密码修改成功,请重新登录"})
|
||||
}
|
||||
|
||||
// meUserBody 统一的用户信息响应体(Me/Login/Refresh 共用形状)
|
||||
func meUserBody(user *model.User) gin.H {
|
||||
// meUserBody 统一的用户信息响应体。boardIDs 为板块管理员被授权的板块,
|
||||
// 供前端渲染管理菜单与前台审核按钮;其他角色为空数组
|
||||
func meUserBody(user *model.User, boardIDs []uint) gin.H {
|
||||
if boardIDs == nil {
|
||||
boardIDs = []uint{}
|
||||
}
|
||||
return gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
@@ -242,6 +255,7 @@ func meUserBody(user *model.User) gin.H {
|
||||
"signature": user.Signature,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
"board_ids": boardIDs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,5 +282,10 @@ func (h *Handlers) Me(c *gin.Context) {
|
||||
if n, err := h.Notification.UnreadCount(claims.ID); err == nil {
|
||||
unread = n
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"user": meUserBody(user), "unread_count": unread})
|
||||
// 板块管理员带出授权板块,供前端菜单/按钮按板块范围渲染
|
||||
var boardIDs []uint
|
||||
if user.Role == model.RoleBoardAdmin {
|
||||
boardIDs, _ = h.Auth.GetUserBoardIDs(claims.ID)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"user": meUserBody(user, boardIDs), "unread_count": unread})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -19,6 +21,18 @@ func (h *Handlers) PostComments(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
|
||||
|
||||
// 待审/被拒帖子的评论不对公众开放
|
||||
var viewerID uint
|
||||
var loadActor func() *service.Actor
|
||||
if claims := middleware.CurrentUser(c); claims != nil {
|
||||
viewerID = claims.ID
|
||||
loadActor = h.actorLoader(claims.ID)
|
||||
}
|
||||
if err := h.Post.EnsurePostVisible(uint(id), viewerID, loadActor); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
comments, floors, totalComments, err := h.Comment.ListFloorPaged(uint(id), page, size)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
@@ -26,7 +40,7 @@ func (h *Handlers) PostComments(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"comments": comments,
|
||||
"total": floors, // 楼层数(主评论数)→ 前端分页与楼层号计算
|
||||
"total": floors, // 楼层数(主评论数)→ 前端分页与楼层号计算
|
||||
"total_comments": totalComments, // 全部评论数(含回复)→ 展示徽标
|
||||
"page": page,
|
||||
"size": size,
|
||||
@@ -52,19 +66,26 @@ func (h *Handlers) CreateComment(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
comment, parent, err := h.Comment.Create(claims.ID, uint(id), req.Content, req.ParentID)
|
||||
// 管理团队成员评论免审;普通用户评论进入待审核队列,审核通过时才发业务通知
|
||||
status := model.ContentStatusPending
|
||||
if model.IsStaff(model.Role(claims.Role)) {
|
||||
status = model.ContentStatusPublished
|
||||
}
|
||||
comment, parent, err := h.Comment.Create(claims.ID, uint(id), req.Content, req.ParentID, status)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if parent == nil {
|
||||
// 主评论:通知帖子作者(排除自己评论自己的帖子)
|
||||
if authorID, e := h.Post.GetAuthorID(uint(id)); e == nil {
|
||||
h.Notification.Create(authorID, claims.ID, model.NotificationTypeComment, uint(id), comment.ID, req.Content)
|
||||
if comment.Status == model.ContentStatusPublished {
|
||||
if parent == nil {
|
||||
// 主评论:通知帖子作者(排除自己评论自己的帖子)
|
||||
if authorID, e := h.Post.GetAuthorID(uint(id)); e == nil {
|
||||
h.Notification.Create(authorID, claims.ID, model.NotificationTypeComment, uint(id), comment.ID, req.Content)
|
||||
}
|
||||
} else {
|
||||
// 子回复:通知父评论作者(Notification.Create 内部排除自我通知)
|
||||
h.Notification.Create(parent.UserID, claims.ID, model.NotificationTypeReply, uint(id), comment.ID, req.Content)
|
||||
}
|
||||
} else {
|
||||
// 子回复:通知父评论作者(Notification.Create 内部排除自我通知)
|
||||
h.Notification.Create(parent.UserID, claims.ID, model.NotificationTypeReply, uint(id), comment.ID, req.Content)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"comment": comment})
|
||||
}
|
||||
@@ -77,8 +98,15 @@ func (h *Handlers) DeleteComment(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的评论 ID"})
|
||||
return
|
||||
}
|
||||
if err := h.Comment.Delete(uint(cid), claims.ID, claims.Role); err != nil {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
if err := h.Comment.Delete(h.loadActor(claims.ID), uint(cid), claims.ID); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrCommentNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
case errors.Is(err, service.ErrCommentForbidden):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "已删除"})
|
||||
|
||||
@@ -20,4 +20,5 @@ type Handlers struct {
|
||||
Upload *service.UploadService
|
||||
Setting *service.SettingService
|
||||
AdminUser *service.AdminUserService
|
||||
Moderation *service.ModerationService
|
||||
}
|
||||
|
||||
85
backend/handler/moderation.go
Normal file
85
backend/handler/moderation.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/middleware"
|
||||
"github.com/freefire/jiang13-bbs/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ===== 内容审核队列(RequireStaff 基础鉴权,板块范围由 service 按 Actor 隔离) =====
|
||||
|
||||
// AdminPendingPosts 待审核帖子分页
|
||||
func (h *Handlers) AdminPendingPosts(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
posts, total, err := h.Moderation.PendingPosts(middleware.CurrentActor(c), page, 15)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取待审帖子失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"posts": posts, "total": total, "page": page})
|
||||
}
|
||||
|
||||
// AdminPendingComments 待审核评论分页
|
||||
func (h *Handlers) AdminPendingComments(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
comments, total, err := h.Moderation.PendingComments(middleware.CurrentActor(c), page, 15)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取待审评论失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"comments": comments, "total": total, "page": page})
|
||||
}
|
||||
|
||||
// AdminPendingCounts 当前操作者可见的待审数量(导航角标)
|
||||
func (h *Handlers) AdminPendingCounts(c *gin.Context) {
|
||||
posts, comments := h.Moderation.PendingCounts(middleware.CurrentActor(c))
|
||||
c.JSON(http.StatusOK, gin.H{"posts": posts, "comments": comments})
|
||||
}
|
||||
|
||||
// AdminApprovePost 通过帖子
|
||||
func (h *Handlers) AdminApprovePost(c *gin.Context) {
|
||||
h.execModeration(c, h.Moderation.ApprovePost)
|
||||
}
|
||||
|
||||
// AdminRejectPost 拒绝帖子
|
||||
func (h *Handlers) AdminRejectPost(c *gin.Context) {
|
||||
h.execModeration(c, h.Moderation.RejectPost)
|
||||
}
|
||||
|
||||
// AdminApproveComment 通过评论
|
||||
func (h *Handlers) AdminApproveComment(c *gin.Context) {
|
||||
h.execModeration(c, h.Moderation.ApproveComment)
|
||||
}
|
||||
|
||||
// AdminRejectComment 拒绝评论
|
||||
func (h *Handlers) AdminRejectComment(c *gin.Context) {
|
||||
h.execModeration(c, h.Moderation.RejectComment)
|
||||
}
|
||||
|
||||
// execModeration 统一解析 :id 并映射审核业务错误
|
||||
func (h *Handlers) execModeration(c *gin.Context, fn func(*service.Actor, uint) error) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
|
||||
return
|
||||
}
|
||||
if err := fn(middleware.CurrentActor(c), uint(id)); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "内容不存在"})
|
||||
case errors.Is(err, service.ErrModerationForbidden):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
case errors.Is(err, service.ErrNotPending):
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "操作失败"})
|
||||
}
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "操作成功"})
|
||||
}
|
||||
@@ -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()})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/middleware"
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -88,6 +89,12 @@ func (h *Handlers) UserProfile(c *gin.Context) {
|
||||
streak = cs.Streak
|
||||
}
|
||||
|
||||
// 板块管理员公开其授权板块(角色徽章展示"板块管理员 · 板块名")
|
||||
var boardIDs []uint
|
||||
if user.Role == model.RoleBoardAdmin {
|
||||
boardIDs, _ = h.Auth.GetUserBoardIDs(user.ID)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
@@ -96,6 +103,7 @@ func (h *Handlers) UserProfile(c *gin.Context) {
|
||||
"avatar": user.Avatar,
|
||||
"signature": user.Signature,
|
||||
"role": user.Role,
|
||||
"board_ids": boardIDs,
|
||||
"created_at": user.CreatedAt,
|
||||
},
|
||||
"stats": gin.H{
|
||||
|
||||
Reference in New Issue
Block a user