Files
jiang13-bbs/backend/handler/post.go
freefire a7b6421840 feat: 实现完整的板块级RBAC内容审核系统
- 新增站长/超级管理员/管理员/板块管理员四级角色体系
- 实现实时权限快照加载与细粒度权限校验
- 新增内容审核队列与前后端页面
- 重构用户权限管理与站点管理逻辑
- 新增评论/帖子审核状态与通知推送
- 优化前端权限控制与角色徽章展示
- 修正数据库迁移与默认值问题
2026-09-15 04:47:34 +08:00

216 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
)
// Posts 获取帖子列表
func (h *Handlers) Posts(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
boardID, _ := strconv.ParseUint(c.Query("board_id"), 10, 64)
sort := c.DefaultQuery("sort", "latest")
keyword := c.Query("keyword")
recommended := c.Query("recommended") == "true"
items, total, err := h.Post.List(service.PostListQuery{
BoardID: uint(boardID),
Page: page,
Size: size,
Sort: sort,
Keyword: keyword,
Recommended: recommended,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 填充点赞状态(仅登录用户)
if claims := middleware.CurrentUser(c); claims != nil {
ids := make([]uint, 0, len(items))
for _, p := range items {
ids = append(ids, p.ID)
}
likedMap := h.Like.BatchHasLiked(ids, claims.ID)
for i := range items {
items[i].Liked = likedMap[items[i].ID]
}
}
c.JSON(http.StatusOK, gin.H{
"posts": items,
"total": total,
"page": page,
"size": size,
})
}
// PostDetail 获取帖子详情
func (h *Handlers) PostDetail(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的帖子 ID"})
return
}
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 != nil {
post.Liked = h.Like.HasLiked(post.ID, claims.ID)
}
c.JSON(http.StatusOK, gin.H{"post": post})
}
// CreatePostRequest 发帖请求
type CreatePostRequest struct {
BoardID uint `json:"board_id" binding:"required"`
Title string `json:"title" binding:"required,min=1,max=256"`
Content string `json:"content" binding:"required,min=1"`
Tags string `json:"tags"`
PostType string `json:"post_type"`
}
// CreatePost 创建帖子
func (h *Handlers) CreatePost(c *gin.Context) {
claims := middleware.CurrentUser(c)
var req CreatePostRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
postType := req.PostType
if postType == "" {
postType = "normal"
}
// 管理团队成员发帖免审直发;普通用户进入待审核队列。
// 角色变更会强制 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
}
c.JSON(http.StatusOK, gin.H{"post": post})
}
// UpdatePostRequest 更新帖子请求
type UpdatePostRequest struct {
Title string `json:"title" binding:"omitempty,min=1,max=256"`
Content string `json:"content" binding:"omitempty,min=1"`
Tags string `json:"tags"`
}
// UpdatePost 编辑帖子
func (h *Handlers) UpdatePost(c *gin.Context) {
claims := middleware.CurrentUser(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的帖子 ID"})
return
}
var req UpdatePostRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
actor := h.loadActor(claims.ID)
post, err := h.Post.Update(actor, uint(id), claims.ID, req.Title, req.Content, req.Tags)
if err != nil {
respondPostModError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"post": post})
}
// DeletePost 删除帖子
func (h *Handlers) DeletePost(c *gin.Context) {
claims := middleware.CurrentUser(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的帖子 ID"})
return
}
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 切换帖子置顶(管理员及以上,板块管理员无此权限)
func (h *Handlers) TogglePin(c *gin.Context) {
if !h.requireAdminOrAbove(c) {
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的帖子 ID"})
return
}
pinned, err := h.Post.TogglePin(uint(id))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"pinned": pinned})
}
// ToggleRecommend 切换帖子加精(管理员及以上,板块管理员无此权限)
func (h *Handlers) ToggleRecommend(c *gin.Context) {
if !h.requireAdminOrAbove(c) {
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的帖子 ID"})
return
}
recommended, err := h.Post.ToggleRecommend(uint(id))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
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()})
}
}