- 新增 smartDiff 差异计算与 EditHistoryModal 编辑历史弹窗,替换原 CommentEditHistoryModal - 新增 PostEditedMark 编辑标记展示 - 新增内容锁定机制(content_lock)防止并发编辑冲突 - 审核与通知服务适配
249 lines
8.5 KiB
Go
249 lines
8.5 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"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// ===== 内容审核队列(RequireStaff 基础鉴权,板块范围由 service 按 Actor 隔离) =====
|
||
|
||
// AdminPendingPosts 待审核帖子分页
|
||
func (h *Handlers) AdminPendingPosts(c *gin.Context) {
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
posts, total, err := h.Moderation.PendingPosts(middleware.CurrentActor(c), page, 15)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取待审帖子失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"posts": nonNilSlice(posts), "total": total, "page": page})
|
||
}
|
||
|
||
// AdminPendingComments 待审核评论分页
|
||
func (h *Handlers) AdminPendingComments(c *gin.Context) {
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
comments, total, err := h.Moderation.PendingComments(middleware.CurrentActor(c), page, 15)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取待审评论失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"comments": nonNilSlice(comments), "total": total, "page": page})
|
||
}
|
||
|
||
// AdminPendingCounts 当前操作者可见的待审数量(导航角标)
|
||
func (h *Handlers) AdminPendingCounts(c *gin.Context) {
|
||
posts, comments := h.Moderation.PendingCounts(middleware.CurrentActor(c))
|
||
c.JSON(http.StatusOK, gin.H{"posts": posts, "comments": comments})
|
||
}
|
||
|
||
// AdminApprovePost 通过帖子
|
||
func (h *Handlers) AdminApprovePost(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil || id == 0 {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
|
||
return
|
||
}
|
||
actor := middleware.CurrentActor(c)
|
||
boardID, err := h.Moderation.ApprovePost(actor, uint(id))
|
||
if err != nil {
|
||
h.writeModerationError(c, err)
|
||
return
|
||
}
|
||
h.broadcastFeedChanged("post", uint(id), boardID, actor.ID)
|
||
h.broadcastModerationChanged()
|
||
c.JSON(http.StatusOK, gin.H{"message": "操作成功"})
|
||
}
|
||
|
||
// AdminRejectPost 拒绝帖子
|
||
func (h *Handlers) AdminRejectPost(c *gin.Context) {
|
||
h.execModeration(c, h.Moderation.RejectPost)
|
||
}
|
||
|
||
// AdminApproveComment 通过评论
|
||
func (h *Handlers) AdminApproveComment(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil || id == 0 {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
|
||
return
|
||
}
|
||
actor := middleware.CurrentActor(c)
|
||
postID, boardID, err := h.Moderation.ApproveComment(actor, uint(id))
|
||
if err != nil {
|
||
h.writeModerationError(c, err)
|
||
return
|
||
}
|
||
// 抽奖帖主评通过审核后入池(无评论者 IP,回退其最近登录 IP)
|
||
var cm model.Comment
|
||
if e := model.DB.Select("id", "post_id", "user_id", "parent_id").First(&cm, uint(id)).Error; e == nil && cm.ParentID == nil {
|
||
ip := h.Post.LatestSuccessLoginIP(cm.UserID)
|
||
h.Post.EnsureLotteryEntry(cm.PostID, cm.UserID, cm.ID, ip)
|
||
}
|
||
h.broadcastFeedChanged("comment", postID, boardID, actor.ID)
|
||
h.broadcastModerationChanged()
|
||
c.JSON(http.StatusOK, gin.H{"message": "操作成功"})
|
||
}
|
||
|
||
// AdminRejectComment 拒绝评论
|
||
func (h *Handlers) AdminRejectComment(c *gin.Context) {
|
||
h.execModeration(c, h.Moderation.RejectComment)
|
||
}
|
||
|
||
// ===== 内容库(帖子/评论 + 已删除回收站) =====
|
||
|
||
func (h *Handlers) AdminContentCounts(c *gin.Context) {
|
||
c.JSON(http.StatusOK, h.Moderation.AdminContentCounts(middleware.CurrentActor(c)))
|
||
}
|
||
|
||
func (h *Handlers) AdminListPosts(c *gin.Context) {
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
items, total, err := h.Moderation.ListAdminPosts(
|
||
middleware.CurrentActor(c), c.Query("status"), c.Query("q"), page,
|
||
)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取帖子列表失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"posts": nonNilSlice(items), "total": total, "page": page})
|
||
}
|
||
|
||
func (h *Handlers) AdminListComments(c *gin.Context) {
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
items, total, err := h.Moderation.ListAdminComments(
|
||
middleware.CurrentActor(c), c.Query("status"), c.Query("q"), page,
|
||
)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取评论列表失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"comments": nonNilSlice(items), "total": total, "page": page})
|
||
}
|
||
|
||
func (h *Handlers) AdminSoftDeletePost(c *gin.Context) {
|
||
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) {
|
||
h.execContentAction(c, h.Moderation.RestorePost)
|
||
}
|
||
|
||
func (h *Handlers) AdminPurgePost(c *gin.Context) {
|
||
h.execContentAction(c, h.Moderation.PurgePost)
|
||
}
|
||
|
||
func (h *Handlers) AdminSoftDeleteComment(c *gin.Context) {
|
||
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)
|
||
if err := h.Moderation.SoftDeleteComment(actor, uint(id), body.DeleteType, body.DeleteReason); err != nil {
|
||
h.writeModerationError(c, err)
|
||
return
|
||
}
|
||
h.broadcastModerationChanged()
|
||
c.JSON(http.StatusOK, gin.H{"message": "操作成功"})
|
||
}
|
||
|
||
func (h *Handlers) AdminRestoreComment(c *gin.Context) {
|
||
h.execContentAction(c, h.Moderation.RestoreComment)
|
||
}
|
||
|
||
func (h *Handlers) AdminPurgeComment(c *gin.Context) {
|
||
h.execContentAction(c, h.Moderation.PurgeComment)
|
||
}
|
||
|
||
func (h *Handlers) execContentAction(c *gin.Context, fn func(*service.Actor, uint) error) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil || id == 0 {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
|
||
return
|
||
}
|
||
if err := fn(middleware.CurrentActor(c), uint(id)); err != nil {
|
||
h.writeModerationError(c, err)
|
||
return
|
||
}
|
||
h.broadcastModerationChanged()
|
||
c.JSON(http.StatusOK, gin.H{"message": "操作成功"})
|
||
}
|
||
|
||
// writeModerationError 映射审核/内容库业务错误为 HTTP 响应
|
||
func (h *Handlers) writeModerationError(c *gin.Context, err error) {
|
||
switch {
|
||
case errors.Is(err, gorm.ErrRecordNotFound), errors.Is(err, service.ErrContentNotFound):
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "内容不存在"})
|
||
case errors.Is(err, service.ErrModerationForbidden),
|
||
errors.Is(err, service.ErrAuthorProtected),
|
||
errors.Is(err, service.ErrPurgeOwnerOnly):
|
||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||
case errors.Is(err, service.ErrNotPending),
|
||
errors.Is(err, service.ErrContentNotDeleted):
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
default:
|
||
msg := err.Error()
|
||
if msg == "帖子已在回收站" || msg == "帖子未被删除" ||
|
||
msg == "评论已在回收站" || msg == "评论未被删除" ||
|
||
msg == "无效的删除类型" || msg == "请填写删除理由" ||
|
||
msg == "删除理由不能超过 200 字" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
|
||
return
|
||
}
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "操作失败"})
|
||
}
|
||
}
|
||
|
||
// execModeration 统一解析 :id 并映射审核业务错误(拒绝等无额外副作用的操作)
|
||
func (h *Handlers) execModeration(c *gin.Context, fn func(*service.Actor, uint) error) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil || id == 0 {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
|
||
return
|
||
}
|
||
if err := fn(middleware.CurrentActor(c), uint(id)); err != nil {
|
||
h.writeModerationError(c, err)
|
||
return
|
||
}
|
||
h.broadcastModerationChanged()
|
||
c.JSON(http.StatusOK, gin.H{"message": "操作成功"})
|
||
}
|