Files
jiang13-bbs/backend/handler/comment.go

245 lines
8.5 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
}
if allow, err := h.Setting.AllowComments(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "评论暂时不可用"})
return
} else if !allow {
c.JSON(http.StatusOK, gin.H{
"comments": []any{},
"total": 0,
"total_comments": 0,
"page": page,
"size": size,
})
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)
}
// 抽奖帖:主评入池并记录客户端 IP(同 IP 去重)
h.Post.EnsureLotteryEntry(uint(id), claims.ID, comment.ID, c.ClientIP())
} 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})
}
// UpdateComment 编辑评论(作者或版主)
func (h *Handlers) UpdateComment(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
}
var body struct {
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
node, err := h.Comment.Update(h.loadActor(claims.ID), uint(cid), claims.ID, body.Content)
if 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()})
case errors.Is(err, service.ErrCommentDeleted),
errors.Is(err, service.ErrCommentNotChanged):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
default:
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
return
}
c.JSON(http.StatusOK, gin.H{"comment": node})
}
// 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
}
var body struct {
DeleteType string `json:"delete_type"`
DeleteReason string `json:"delete_reason"`
}
_ = c.ShouldBindJSON(&body)
if err := h.Comment.Delete(h.loadActor(claims.ID), uint(cid), claims.ID, service.DeleteCommentOpts{
DeleteType: body.DeleteType,
DeleteReason: body.DeleteReason,
}); err != nil {
switch {
case errors.Is(err, service.ErrCommentNotFound):
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
case errors.Is(err, service.ErrCommentForbidden),
errors.Is(err, service.ErrAuthorProtected):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
case errors.Is(err, service.ErrCommentDeleteMeta),
errors.Is(err, service.ErrCommentInvalidType):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
default:
msg := err.Error()
if msg == "删除理由不能超过 200 字" {
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
}
return
}
c.JSON(http.StatusOK, gin.H{"message": "已删除"})
}
// RestoreComment 恢复软删评论(自删作者或版主)
func (h *Handlers) RestoreComment(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.Restore(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),
errors.Is(err, service.ErrAuthorProtected):
c.JSON(http.StatusForbidden, gin.H{"error": "无权限恢复此评论"})
case errors.Is(err, service.ErrCommentNotDeleted):
c.JSON(http.StatusBadRequest, 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),
errors.Is(err, service.ErrPurgeOwnerOnly):
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": "已彻底删除"})
}