fix: 空评论列表 500,并完善撤回审计与角色说明
空切片 JSON 统一为数组;撤回保留正文供站长阅读,成员侧居中提示与重新编辑;头像支持拖放;角色弹窗补全职责说明并汇入封禁。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 新建公告
|
||||
|
||||
@@ -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)})
|
||||
}
|
||||
|
||||
@@ -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 未读汇总
|
||||
|
||||
@@ -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,
|
||||
|
||||
9
backend/handler/jsonutil.go
Normal file
9
backend/handler/jsonutil.go
Normal 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
|
||||
}
|
||||
@@ -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 当前操作者可见的待审数量(导航角标)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 + 物理文件)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -318,5 +318,7 @@ type ChatMessage struct {
|
||||
CreatedAt time.Time `gorm:"index:idx_chat_room_created,priority:2" json:"created_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Sender User `gorm:"foreignKey:SenderID" json:"sender,omitempty"`
|
||||
Sender User `gorm:"foreignKey:SenderID" json:"sender,omitempty"`
|
||||
// RecalledBy=0 时不建 FK;Preload 仅用于已撤回消息展示撤回者昵称
|
||||
Recaller *User `gorm:"foreignKey:RecalledBy;constraint:-" json:"recaller,omitempty"`
|
||||
}
|
||||
|
||||
@@ -652,8 +652,9 @@ func (s *AdminUserService) ListAuditMessages(actor *Actor, userID uint, page, si
|
||||
}
|
||||
content := m.Content
|
||||
recalled := m.RecalledAt != nil
|
||||
// 历史数据:早期撤回曾清空正文,无法还原
|
||||
if recalled && content == "" {
|
||||
content = "(已撤回的消息)"
|
||||
content = "(撤回时未保留正文)"
|
||||
}
|
||||
it := AdminAuditMessageItem{
|
||||
ID: m.ID,
|
||||
|
||||
@@ -153,6 +153,10 @@ func (s *ChatService) toViews(rooms []model.ChatRoom, userID uint) []RoomView {
|
||||
unread := s.unreadCounts(userID, ids)
|
||||
views := make([]RoomView, 0, len(rooms))
|
||||
for _, r := range rooms {
|
||||
// 会话列表预览不向前台下发已撤回正文
|
||||
if r.LastMessage != nil && r.LastMessage.RecalledAt != nil {
|
||||
r.LastMessage.Content = ""
|
||||
}
|
||||
v := RoomView{ChatRoom: r, UnreadCount: unread[r.ID]}
|
||||
if r.IsDefault {
|
||||
v.Pinned = true
|
||||
@@ -887,7 +891,7 @@ func (s *ChatService) ListMessages(userID, roomID, beforeID uint, size int, over
|
||||
q = q.Where("id < ?", beforeID)
|
||||
}
|
||||
var msgs []model.ChatMessage
|
||||
if err := q.Order("id DESC").Limit(size + 1).Preload("Sender").Find(&msgs).Error; err != nil {
|
||||
if err := q.Order("id DESC").Limit(size + 1).Preload("Sender").Preload("Recaller").Find(&msgs).Error; err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
hasMore := len(msgs) > size
|
||||
@@ -897,7 +901,7 @@ func (s *ChatService) ListMessages(userID, roomID, beforeID uint, size int, over
|
||||
for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 {
|
||||
msgs[i], msgs[j] = msgs[j], msgs[i]
|
||||
}
|
||||
// 对非监管者清空已撤回正文
|
||||
// 已撤回正文:仅监管者(站长/超管)保留,供聊天界面审计展示;普通成员清空
|
||||
if !oversee {
|
||||
for i := range msgs {
|
||||
if msgs[i].RecalledAt != nil {
|
||||
@@ -908,7 +912,7 @@ func (s *ChatService) ListMessages(userID, roomID, beforeID uint, size int, over
|
||||
return msgs, hasMore, nil
|
||||
}
|
||||
|
||||
// RecallMessage 撤回消息:群主可撤自己的;监管者可撤任意;标记 RecalledAt 不清行
|
||||
// RecallMessage 撤回消息:群主可撤自己的;监管者可撤任意;标记 RecalledAt,正文保留供审计
|
||||
func (s *ChatService) RecallMessage(operatorID, roomID, messageID uint, oversee bool) (*model.ChatMessage, error) {
|
||||
if _, err := s.getRoom(roomID); err != nil {
|
||||
return nil, err
|
||||
@@ -935,17 +939,16 @@ func (s *ChatService) RecallMessage(operatorID, roomID, messageID uint, oversee
|
||||
}
|
||||
}
|
||||
now := time.Now()
|
||||
// 只打撤回标记,保留正文供站长审计;前台 ListMessages 对非监管者置空 content
|
||||
if err := s.db.Model(&msg).Updates(map[string]interface{}{
|
||||
"recalled_at": now,
|
||||
"recalled_by": operatorID,
|
||||
"content": "",
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg.RecalledAt = &now
|
||||
msg.RecalledBy = operatorID
|
||||
msg.Content = ""
|
||||
_ = s.db.Preload("Sender").First(&msg, msg.ID)
|
||||
_ = s.db.Preload("Sender").Preload("Recaller").First(&msg, msg.ID)
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -225,6 +225,9 @@ func (s *CommentService) ListByUser(userID uint, page, size int) ([]UserCommentI
|
||||
if err := query.Order("comments.created_at DESC").Offset(offset).Limit(size).Scan(&items).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if items == nil {
|
||||
items = []UserCommentItem{}
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user