feat: 评论/帖子编辑历史与编辑标记,内容锁定

- 新增 smartDiff 差异计算与 EditHistoryModal 编辑历史弹窗,替换原 CommentEditHistoryModal
- 新增 PostEditedMark 编辑标记展示
- 新增内容锁定机制(content_lock)防止并发编辑冲突
- 审核与通知服务适配
This commit is contained in:
2026-09-25 23:41:45 +08:00
parent cab8230803
commit 5270fee2b7
40 changed files with 2521 additions and 350 deletions

View File

@@ -2,6 +2,7 @@ package handler
import (
"errors"
"fmt"
"net/http"
"strconv"
@@ -83,7 +84,7 @@ func (h *Handlers) PostComments(c *gin.Context) {
// CreateCommentRequest 评论请求
type CreateCommentRequest struct {
Content string `json:"content" binding:"required,min=1"`
Content string `json:"content" binding:"required,min=1,max=5000"`
ParentID *uint `json:"parent_id"` // 非空 = 回复该评论(发为其子评论)
}
@@ -109,11 +110,24 @@ func (h *Handlers) CreateComment(c *gin.Context) {
}
// staff 直发;已过审用户在站点开启免审时可直发;其余进待审队列(通过时才发业务通知)
status := h.resolvePublishStatus(claims.ID, claims.Role)
comment, parent, err := h.Comment.Create(claims.ID, uint(id), req.Content, req.ParentID, status)
comment, parent, necroPoints, err := h.Comment.Create(claims.ID, uint(id), req.Content, req.ParentID, status, model.Role(claims.Role))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
switch {
case errors.Is(err, service.ErrPostReplyLocked):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
case errors.Is(err, service.ErrInsufficientPoints):
// 旧帖回复扣分余额不足(文案含具体规则)
c.JSON(http.StatusPaymentRequired, gin.H{"error": err.Error()})
default:
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
return
}
// 旧帖回复扣分:系统提醒本人(ActorID=0,不经过自我通知过滤)
if necroPoints > 0 {
h.Notification.CreateSystem(claims.ID, model.NotificationTypeNecroReply, uint(id), comment.ID,
fmt.Sprintf("您回复了长期没有新回复的旧帖,已扣除 %d 积分", necroPoints))
}
if comment.Status == model.ContentStatusPublished {
if parent == nil {
// 主评论:通知帖子作者(排除自己评论自己的帖子)
@@ -146,7 +160,7 @@ func (h *Handlers) UpdateComment(c *gin.Context) {
return
}
var body struct {
Content string `json:"content"`
Content string `json:"content" binding:"omitempty,min=1,max=5000"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
@@ -157,7 +171,9 @@ func (h *Handlers) UpdateComment(c *gin.Context) {
switch {
case errors.Is(err, service.ErrCommentNotFound):
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
case errors.Is(err, service.ErrCommentForbidden):
case errors.Is(err, service.ErrCommentForbidden),
errors.Is(err, service.ErrCommentPostLocked),
errors.Is(err, service.ErrCommentEditLocked):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
case errors.Is(err, service.ErrCommentDeleted),
errors.Is(err, service.ErrCommentNotChanged):
@@ -256,3 +272,39 @@ func (h *Handlers) PurgeComment(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{"message": "已彻底删除"})
}
// CommentEditHistory 评论编辑历史(登录即可见;分页)
func (h *Handlers) CommentEditHistory(c *gin.Context) {
claims := middleware.CurrentUser(c)
cid, err := strconv.ParseUint(c.Param("cid"), 10, 64)
if err != nil || cid == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的评论 ID"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
items, total, err := h.Comment.ListEditHistory(h.loadActor(claims.ID), uint(cid), page, size)
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()})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取编辑历史失败"})
}
return
}
if page < 1 {
page = 1
}
if size < 1 || size > 50 {
size = 10
}
c.JSON(http.StatusOK, gin.H{
"items": items,
"total": total,
"page": page,
"size": size,
})
}