Files
jiang13-bbs/backend/handler/comment.go
freefire bcb4f6a603 feat(moderation): 帖评删除占位感知与硬删清场
管理删帖须填类型与理由并展示专用页;评论软删占位、版主可硬删不占楼。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 07:01:04 +08:00

152 lines
5.4 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"
)
// PostComments 获取帖子评论树(楼层分页:主评论算楼层,每楼附带全部子回复,时间正序)
func (h *Handlers) PostComments(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的帖子 ID"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
// 待审/被拒帖子的评论不对公众开放
var viewerID uint
var actor *service.Actor
var loadActor func() *service.Actor
if claims := middleware.CurrentUser(c); claims != nil {
viewerID = claims.ID
loadActor = h.actorLoader(claims.ID)
if a, err := h.Auth.LoadActor(claims.ID); err == nil {
actor = a
}
}
if err := h.Post.EnsurePostVisible(uint(id), viewerID, loadActor); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"})
return
}
boardID, _ := h.Post.GetBoardID(uint(id))
comments, floors, totalComments, err := h.Comment.ListFloorPaged(uint(id), boardID, viewerID, actor, page, size)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"comments": nonNilSlice(comments),
"total": floors, // 楼层数(主评论数)→ 前端分页与楼层号计算
"total_comments": totalComments, // 全部评论数(含回复)→ 展示徽标
"page": page,
"size": size,
})
}
// CreateCommentRequest 评论请求
type CreateCommentRequest struct {
Content string `json:"content" binding:"required,min=1"`
ParentID *uint `json:"parent_id"` // 非空 = 回复该评论(发为其子评论)
}
// CreateComment 创建评论(主评论或子回复)
func (h *Handlers) CreateComment(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 allow, err := h.Setting.AllowComments(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "评论暂时不可用"})
return
} else if !allow {
c.JSON(http.StatusForbidden, gin.H{"error": "站点已关闭评论"})
return
}
var req CreateCommentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// staff 直发;已过审用户在站点开启免审时可直发;其余进待审队列(通过时才发业务通知)
status := h.resolvePublishStatus(claims.ID, claims.Role)
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 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)
}
if boardID, e := h.Post.GetBoardID(uint(id)); e == nil {
h.broadcastFeedChanged("comment", uint(id), boardID, claims.ID)
}
} else if comment.Status == model.ContentStatusPending {
if boardID, e := h.Post.GetBoardID(uint(id)); e == nil {
h.notifyPendingReview(boardID, claims.ID, uint(id), comment.ID, "有新评论待审核")
}
}
c.JSON(http.StatusOK, gin.H{"comment": comment})
}
// DeleteComment 软删评论(占位)
func (h *Handlers) DeleteComment(c *gin.Context) {
claims := middleware.CurrentUser(c)
cid, err := strconv.ParseUint(c.Param("cid"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的评论 ID"})
return
}
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": "已删除"})
}
// PurgeComment 硬删评论及子树(仅版主;不占位)
func (h *Handlers) PurgeComment(c *gin.Context) {
claims := middleware.CurrentUser(c)
cid, err := strconv.ParseUint(c.Param("cid"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的评论 ID"})
return
}
if err := h.Comment.Purge(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": "已彻底删除"})
}