feat(moderation): 帖评删除占位感知与硬删清场

管理删帖须填类型与理由并展示专用页;评论软删占位、版主可硬删不占楼。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 07:01:04 +08:00
parent 2b27703e6c
commit bcb4f6a603
15 changed files with 825 additions and 116 deletions

View File

@@ -66,6 +66,13 @@ func (h *Handlers) CreateComment(c *gin.Context) {
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()})
@@ -99,7 +106,7 @@ func (h *Handlers) CreateComment(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"comment": comment})
}
// DeleteComment 删除评论
// DeleteComment 软删评论(占位)
func (h *Handlers) DeleteComment(c *gin.Context) {
claims := middleware.CurrentUser(c)
cid, err := strconv.ParseUint(c.Param("cid"), 10, 64)
@@ -120,3 +127,25 @@ func (h *Handlers) DeleteComment(c *gin.Context) {
}
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": "已彻底删除"})
}

View File

@@ -6,6 +6,7 @@ import (
"strconv"
"github.com/freefire/jiang13-bbs/middleware"
"github.com/freefire/jiang13-bbs/model"
"github.com/freefire/jiang13-bbs/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -118,7 +119,33 @@ func (h *Handlers) AdminListComments(c *gin.Context) {
}
func (h *Handlers) AdminSoftDeletePost(c *gin.Context) {
h.execContentAction(c, h.Moderation.SoftDeletePost)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
return
}
var body struct {
DeleteType string `json:"delete_type"`
DeleteReason string `json:"delete_reason"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提交删除类型与理由"})
return
}
actor := middleware.CurrentActor(c)
authorID, err := h.Moderation.SoftDeletePost(actor, uint(id), body.DeleteType, body.DeleteReason)
if err != nil {
h.writeModerationError(c, err)
return
}
label := model.PostDeleteTypeLabel(body.DeleteType)
preview := label
if body.DeleteReason != "" {
preview = label + ":" + body.DeleteReason
}
h.Notification.Create(authorID, actor.ID, model.NotificationTypeDeleted, uint(id), 0, preview)
h.broadcastModerationChanged()
c.JSON(http.StatusOK, gin.H{"message": "操作成功"})
}
func (h *Handlers) AdminRestorePost(c *gin.Context) {
@@ -168,7 +195,9 @@ func (h *Handlers) writeModerationError(c *gin.Context, err error) {
default:
msg := err.Error()
if msg == "帖子已在回收站" || msg == "帖子未被删除" ||
msg == "评论已在回收站" || msg == "评论未被删除" {
msg == "评论已在回收站" || msg == "评论未被删除" ||
msg == "无效的删除类型" || msg == "请填写删除理由" ||
msg == "删除理由不能超过 200 字" {
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}

View File

@@ -361,7 +361,7 @@ func (h *Handlers) DownloadPostAttachment(c *gin.Context) {
_ = filepath.Base(path)
}
// DeletePost 删除帖子
// DeletePost 删除帖子(作者自删或 staff 软删;staff 须提交类型与理由)
func (h *Handlers) DeletePost(c *gin.Context) {
claims := middleware.CurrentUser(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
@@ -369,10 +369,30 @@ func (h *Handlers) DeletePost(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的帖子 ID"})
return
}
if err := h.Post.Delete(h.loadActor(claims.ID), uint(id), claims.ID); err != nil {
var body struct {
DeleteType string `json:"delete_type"`
DeleteReason string `json:"delete_reason"`
}
_ = c.ShouldBindJSON(&body)
staffDeleted, authorID, err := h.Post.Delete(
h.loadActor(claims.ID),
uint(id),
claims.ID,
service.DeletePostOpts{DeleteType: body.DeleteType, DeleteReason: body.DeleteReason},
)
if err != nil {
respondPostModError(c, err)
return
}
if staffDeleted {
label := model.PostDeleteTypeLabel(body.DeleteType)
preview := label
if body.DeleteReason != "" {
preview = label + ":" + body.DeleteReason
}
h.Notification.Create(authorID, claims.ID, model.NotificationTypeDeleted, uint(id), 0, preview)
}
c.JSON(http.StatusOK, gin.H{"message": "已删除"})
}