fix: 空评论列表 500,并完善撤回审计与角色说明

空切片 JSON 统一为数组;撤回保留正文供站长阅读,成员侧居中提示与重新编辑;头像支持拖放;角色弹窗补全职责说明并汇入封禁。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 00:23:55 +08:00
parent 42c395a864
commit 8c5de13899
29 changed files with 835 additions and 405 deletions

View File

@@ -102,7 +102,7 @@ func (h *Handlers) AdminUserLoginLogs(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取登录历史失败"})
return
}
c.JSON(http.StatusOK, gin.H{"logs": logs, "total": total, "page": page})
c.JSON(http.StatusOK, gin.H{"logs": nonNilSlice(logs), "total": total, "page": page})
}
// AdminGetUserAudit 站长:用户内容档案摘要
@@ -131,7 +131,7 @@ func (h *Handlers) AdminUserAuditPosts(c *gin.Context) {
respondAdminUserError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"items": items, "total": total, "page": page})
c.JSON(http.StatusOK, gin.H{"items": nonNilSlice(items), "total": total, "page": page})
}
// AdminUserAuditComments 站长:用户评论(含驳回/软删)
@@ -146,7 +146,7 @@ func (h *Handlers) AdminUserAuditComments(c *gin.Context) {
respondAdminUserError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"items": items, "total": total, "page": page})
c.JSON(http.StatusOK, gin.H{"items": nonNilSlice(items), "total": total, "page": page})
}
// AdminUserAuditMessages 站长:用户聊天消息(含撤回/软删)
@@ -161,7 +161,7 @@ func (h *Handlers) AdminUserAuditMessages(c *gin.Context) {
respondAdminUserError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"items": items, "total": total, "page": page})
c.JSON(http.StatusOK, gin.H{"items": nonNilSlice(items), "total": total, "page": page})
}
// parseAdminUserID 解析路径中的用户 ID,失败时直接写出 400

View File

@@ -19,7 +19,7 @@ func (h *Handlers) AnnouncementsList(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取公告失败"})
return
}
c.JSON(http.StatusOK, gin.H{"announcements": list})
c.JSON(http.StatusOK, gin.H{"announcements": nonNilSlice(list)})
}
// AnnouncementDetail 已发布公告详情
@@ -50,7 +50,7 @@ func (h *Handlers) AdminListAnnouncements(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取公告失败"})
return
}
c.JSON(http.StatusOK, gin.H{"announcements": list})
c.JSON(http.StatusOK, gin.H{"announcements": nonNilSlice(list)})
}
// AdminCreateAnnouncement 新建公告

View File

@@ -13,5 +13,5 @@ func (h *Handlers) Boards(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"boards": boards})
c.JSON(http.StatusOK, gin.H{"boards": nonNilSlice(boards)})
}

View File

@@ -98,7 +98,7 @@ func (h *Handlers) ListChatRooms(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取会话列表失败"})
return
}
c.JSON(http.StatusOK, gin.H{"rooms": rooms, "total": total, "page": page, "size": size})
c.JSON(http.StatusOK, gin.H{"rooms": nonNilSlice(rooms), "total": total, "page": page, "size": size})
}
// GetChatRoom 群详情(公开非成员可预览;私密非成员 404)
@@ -273,7 +273,7 @@ func (h *Handlers) ListChatMembers(c *gin.Context) {
c.JSON(chatErrToStatus(err), gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"members": list})
c.JSON(http.StatusOK, gin.H{"members": nonNilSlice(list)})
}
// KickChatMember 群主踢人
@@ -315,7 +315,7 @@ func (h *Handlers) ListChatMessages(c *gin.Context) {
c.JSON(chatErrToStatus(err), gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"messages": msgs, "has_more": hasMore})
c.JSON(http.StatusOK, gin.H{"messages": nonNilSlice(msgs), "has_more": hasMore})
}
// SendChatMessage 发消息
@@ -387,16 +387,30 @@ func (h *Handlers) RecallChatMessage(c *gin.Context) {
c.JSON(chatErrToStatus(err), gin.H{"error": err.Error()})
return
}
data := gin.H{
"id": msg.ID,
"room_id": roomID,
"sender_id": msg.SenderID,
"recalled_by": msg.RecalledBy,
"recalled_at": msg.RecalledAt,
}
if msg.Recaller != nil && msg.Recaller.ID > 0 {
data["recaller"] = gin.H{
"id": msg.Recaller.ID,
"username": msg.Recaller.Username,
"nickname": msg.Recaller.Nickname,
"avatar": msg.Recaller.Avatar,
"role": msg.Recaller.Role,
}
}
h.Hub.BroadcastRoom(realtime.RoomChat(roomID), realtime.Envelope{
Type: realtime.EventChatRecalled,
Data: gin.H{
"id": msg.ID,
"room_id": roomID,
"sender_id": msg.SenderID,
"recalled_by": msg.RecalledBy,
"recalled_at": msg.RecalledAt,
},
Data: data,
})
// 监管者保留正文供界面展示;撤回者本人保留正文供「重新编辑」;其余不回传
if !oversee && msg.SenderID != claims.ID {
msg.Content = ""
}
c.JSON(http.StatusOK, gin.H{"message": msg})
}
@@ -444,7 +458,7 @@ func (h *Handlers) SearchChatUsers(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "搜索失败"})
return
}
c.JSON(http.StatusOK, gin.H{"users": users})
c.JSON(http.StatusOK, gin.H{"users": nonNilSlice(users)})
}
// ChatUnreadSummary 未读汇总

View File

@@ -44,7 +44,7 @@ func (h *Handlers) PostComments(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{
"comments": comments,
"comments": nonNilSlice(comments),
"total": floors, // 楼层数(主评论数)→ 前端分页与楼层号计算
"total_comments": totalComments, // 全部评论数(含回复)→ 展示徽标
"page": page,

View File

@@ -0,0 +1,9 @@
package handler
// nonNilSlice 保证 JSON 序列化为 [] 而非 null(Go nil slice 会编成 null)。
func nonNilSlice[T any](s []T) []T {
if s == nil {
return []T{}
}
return s
}

View File

@@ -21,7 +21,7 @@ func (h *Handlers) AdminPendingPosts(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取待审帖子失败"})
return
}
c.JSON(http.StatusOK, gin.H{"posts": posts, "total": total, "page": page})
c.JSON(http.StatusOK, gin.H{"posts": nonNilSlice(posts), "total": total, "page": page})
}
// AdminPendingComments 待审核评论分页
@@ -32,7 +32,7 @@ func (h *Handlers) AdminPendingComments(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取待审评论失败"})
return
}
c.JSON(http.StatusOK, gin.H{"comments": comments, "total": total, "page": page})
c.JSON(http.StatusOK, gin.H{"comments": nonNilSlice(comments), "total": total, "page": page})
}
// AdminPendingCounts 当前操作者可见的待审数量(导航角标)

View File

@@ -20,7 +20,7 @@ func (h *Handlers) Notifications(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{
"notifications": list,
"notifications": nonNilSlice(list),
"total": total,
"page": page,
"size": size,

View File

@@ -77,7 +77,7 @@ func (h *Handlers) MyMedia(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"attachments": list})
c.JSON(http.StatusOK, gin.H{"attachments": nonNilSlice(list)})
}
// DeleteAttachment 彻底删除本人附件(DB + 物理文件)

View File

@@ -104,6 +104,9 @@ func (h *Handlers) UserProfile(c *gin.Context) {
if user.Role == model.RoleBoardAdmin {
boardIDs, _ = h.Auth.GetUserBoardIDs(user.ID)
}
if boardIDs == nil {
boardIDs = []uint{}
}
c.JSON(http.StatusOK, gin.H{
"user": gin.H{
@@ -122,7 +125,7 @@ func (h *Handlers) UserProfile(c *gin.Context) {
"points": pointsTotal,
"streak": streak,
},
"posts": posts,
"posts": nonNilSlice(posts),
"posts_total": postsTotal,
"page": page,
"size": size,
@@ -152,7 +155,7 @@ func (h *Handlers) UserComments(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{
"comments": comments,
"comments": nonNilSlice(comments),
"total": total,
"page": page,
"size": size,