feat: 评论/帖子编辑历史与编辑标记,内容锁定
- 新增 smartDiff 差异计算与 EditHistoryModal 编辑历史弹窗,替换原 CommentEditHistoryModal - 新增 PostEditedMark 编辑标记展示 - 新增内容锁定机制(content_lock)防止并发编辑冲突 - 审核与通知服务适配
This commit is contained in:
@@ -89,6 +89,7 @@ func (h *Handlers) PostDetail(c *gin.Context) {
|
||||
if claims != nil {
|
||||
post.Liked = h.Like.HasLiked(post.ID, claims.ID)
|
||||
}
|
||||
h.attachNecroReplyHint(claims, post)
|
||||
c.JSON(http.StatusOK, gin.H{"post": post})
|
||||
}
|
||||
|
||||
@@ -96,7 +97,7 @@ func (h *Handlers) PostDetail(c *gin.Context) {
|
||||
type CreatePostRequest struct {
|
||||
BoardID uint `json:"board_id" binding:"required"`
|
||||
Title string `json:"title" binding:"required,min=1,max=256"`
|
||||
Content string `json:"content" binding:"required,min=1"`
|
||||
Content string `json:"content" binding:"required,min=1,max=20000"`
|
||||
Tags string `json:"tags"`
|
||||
PostType string `json:"post_type"`
|
||||
TypeMeta string `json:"type_meta"`
|
||||
@@ -138,7 +139,7 @@ func (h *Handlers) CreatePost(c *gin.Context) {
|
||||
// UpdatePostRequest 更新帖子请求
|
||||
type UpdatePostRequest struct {
|
||||
Title string `json:"title" binding:"omitempty,min=1,max=256"`
|
||||
Content string `json:"content" binding:"omitempty,min=1"`
|
||||
Content string `json:"content" binding:"omitempty,min=1,max=20000"`
|
||||
Tags string `json:"tags"`
|
||||
TypeMeta *string `json:"type_meta"`
|
||||
AttachmentIDs *[]uint `json:"attachment_ids"`
|
||||
@@ -172,6 +173,42 @@ func (h *Handlers) UpdatePost(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"post": post})
|
||||
}
|
||||
|
||||
// PostEditHistory 帖子编辑历史(登录即可见;分页)
|
||||
func (h *Handlers) PostEditHistory(c *gin.Context) {
|
||||
claims := middleware.CurrentUser(c)
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 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.Post.ListEditHistory(h.loadActor(claims.ID), uint(id), page, size)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrPostNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
case errors.Is(err, service.ErrPostForbidden):
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// UnlockPostContent 积分解锁正文
|
||||
func (h *Handlers) UnlockPostContent(c *gin.Context) {
|
||||
claims := middleware.CurrentUser(c)
|
||||
@@ -239,6 +276,7 @@ func (h *Handlers) UnlockPostPassword(c *gin.Context) {
|
||||
if claims != nil {
|
||||
post.Liked = h.Like.HasLiked(post.ID, claims.ID)
|
||||
}
|
||||
h.attachNecroReplyHint(claims, post)
|
||||
c.JSON(http.StatusOK, gin.H{"post": post})
|
||||
}
|
||||
|
||||
@@ -539,6 +577,52 @@ func (h *Handlers) ToggleRecommend(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"recommended": rec})
|
||||
}
|
||||
|
||||
// TogglePostLock 切换帖子手动锁定(管理员及以上;锁定后普通用户不可编辑/回复,staff 豁免)
|
||||
func (h *Handlers) TogglePostLock(c *gin.Context) {
|
||||
if !h.requireAdminOrAbove(c) {
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的帖子 ID"})
|
||||
return
|
||||
}
|
||||
locked, err := h.Post.ToggleLock(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"locked": locked})
|
||||
}
|
||||
|
||||
// attachNecroReplyHint 旧帖回复确认提示:仅当 viewer 已登录、非 staff、规则开启、
|
||||
// 帖子判定为旧帖、且其评论将直发(待审不扣分不提醒,不弹窗)时返回
|
||||
func (h *Handlers) attachNecroReplyHint(claims *service.UserClaims, detail *service.PostDetail) {
|
||||
if claims == nil || detail == nil || detail.Tombstone || detail.NecroReply != nil {
|
||||
return
|
||||
}
|
||||
if model.IsStaff(model.Role(claims.Role)) {
|
||||
return
|
||||
}
|
||||
afterHours, err1 := h.Setting.NecroReplyAfterHours()
|
||||
penalty, err2 := h.Setting.NecroReplyPenalty()
|
||||
if err1 != nil || err2 != nil || afterHours <= 0 || penalty <= 0 {
|
||||
return
|
||||
}
|
||||
// 旧帖基准:最后回复时间;无回复(null)回落发帖时间
|
||||
baseline := detail.CreatedAt
|
||||
if detail.LastReplyAt != nil {
|
||||
baseline = *detail.LastReplyAt
|
||||
}
|
||||
if !service.IsNecroReply(baseline, afterHours, time.Now()) {
|
||||
return
|
||||
}
|
||||
if h.resolvePublishStatus(claims.ID, claims.Role) != model.ContentStatusPublished {
|
||||
return
|
||||
}
|
||||
detail.NecroReply = &service.NecroReplyHint{AfterHours: afterHours, Penalty: penalty}
|
||||
}
|
||||
|
||||
// requireAdminOrAbove 置顶/加精仅管理员及以上可用;以 DB Actor 为准
|
||||
func (h *Handlers) requireAdminOrAbove(c *gin.Context) bool {
|
||||
claims := middleware.CurrentUser(c)
|
||||
@@ -556,7 +640,9 @@ func respondPostModError(c *gin.Context, err error) {
|
||||
case errors.Is(err, service.ErrPostNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
case errors.Is(err, service.ErrPostForbidden),
|
||||
errors.Is(err, service.ErrAuthorProtected):
|
||||
errors.Is(err, service.ErrAuthorProtected),
|
||||
errors.Is(err, service.ErrPostLocked),
|
||||
errors.Is(err, service.ErrPostEditLocked):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
case errors.Is(err, service.ErrPollOptionsLocked),
|
||||
errors.Is(err, service.ErrLotteryPrizesLocked):
|
||||
|
||||
Reference in New Issue
Block a user