统一投票/悬赏/抽奖截止与状态展示,抽奖支持多奖品行;发帖页悬赏积分对齐表格输入,并在余额不足时于底部就地提示。 Co-authored-by: Cursor <cursoragent@cursor.com>
227 lines
8.1 KiB
Go
227 lines
8.1 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"
|
||
)
|
||
|
||
// 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)
|
||
}
|
||
// 抽奖帖:主评入池并记录客户端 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):
|
||
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):
|
||
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):
|
||
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": "已彻底删除"})
|
||
}
|