feat: 实时消息与管理端运营能力

新增群聊/私信与公共 WebSocket 总线,站长用户内容档案,并将通知与待审角标改为推送驱动;同步精简管理仪表盘并修复主题换肤 DOM 冲突。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-15 22:58:16 +08:00
parent 29db9ae9b5
commit 42c395a864
55 changed files with 7607 additions and 346 deletions

View File

@@ -43,7 +43,20 @@ func (h *Handlers) AdminPendingCounts(c *gin.Context) {
// AdminApprovePost 通过帖子
func (h *Handlers) AdminApprovePost(c *gin.Context) {
h.execModeration(c, h.Moderation.ApprovePost)
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 拒绝帖子
@@ -53,7 +66,20 @@ func (h *Handlers) AdminRejectPost(c *gin.Context) {
// AdminApproveComment 通过评论
func (h *Handlers) AdminApproveComment(c *gin.Context) {
h.execModeration(c, h.Moderation.ApproveComment)
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
}
h.broadcastFeedChanged("comment", postID, boardID, actor.ID)
h.broadcastModerationChanged()
c.JSON(http.StatusOK, gin.H{"message": "操作成功"})
}
// AdminRejectComment 拒绝评论
@@ -61,7 +87,21 @@ func (h *Handlers) AdminRejectComment(c *gin.Context) {
h.execModeration(c, h.Moderation.RejectComment)
}
// execModeration 统一解析 :id 并映射审核业务错误
// writeModerationError 映射审核业务错误为 HTTP 响应
func (h *Handlers) writeModerationError(c *gin.Context, err error) {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
c.JSON(http.StatusNotFound, gin.H{"error": "内容不存在"})
case errors.Is(err, service.ErrModerationForbidden):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
case errors.Is(err, service.ErrNotPending):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
default:
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 {
@@ -69,17 +109,9 @@ func (h *Handlers) execModeration(c *gin.Context, fn func(*service.Actor, uint)
return
}
if err := fn(middleware.CurrentActor(c), uint(id)); err != nil {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
c.JSON(http.StatusNotFound, gin.H{"error": "内容不存在"})
case errors.Is(err, service.ErrModerationForbidden):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
case errors.Is(err, service.ErrNotPending):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "操作失败"})
}
h.writeModerationError(c, err)
return
}
h.broadcastModerationChanged()
c.JSON(http.StatusOK, gin.H{"message": "操作成功"})
}