新增群聊/私信与公共 WebSocket 总线,站长用户内容档案,并将通知与待审角标改为推送驱动;同步精简管理仪表盘并修复主题换肤 DOM 冲突。 Co-authored-by: Cursor <cursoragent@cursor.com>
232 lines
6.8 KiB
Go
232 lines
6.8 KiB
Go
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"
|
||
|
||
var viewerID uint
|
||
var actor *service.Actor
|
||
if claims := middleware.CurrentUser(c); claims != nil {
|
||
viewerID = claims.ID
|
||
if a, err := h.Auth.LoadActor(claims.ID); err == nil {
|
||
actor = a
|
||
}
|
||
}
|
||
|
||
items, total, err := h.Post.List(service.PostListQuery{
|
||
BoardID: uint(boardID),
|
||
Page: page,
|
||
Size: size,
|
||
Sort: sort,
|
||
Keyword: keyword,
|
||
Recommended: recommended,
|
||
ViewerID: viewerID,
|
||
Actor: actor,
|
||
})
|
||
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
|
||
}
|
||
if post.Status == model.ContentStatusPublished {
|
||
h.broadcastFeedChanged("post", post.ID, post.BoardID, claims.ID)
|
||
} else if post.Status == model.ContentStatusPending {
|
||
h.notifyPendingReview(post.BoardID, claims.ID, post.ID, 0, "有新帖待审核:"+post.Title)
|
||
}
|
||
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()})
|
||
}
|
||
}
|