fix(chat): 撤回消息时回退未读角标,避免当成新消息
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -401,12 +401,12 @@ func (h *Handlers) RecallChatMessage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
oversee := h.chatOversee(claims.ID)
|
||||
msg, err := h.Chat.RecallMessage(claims.ID, roomID, uint(mid), oversee)
|
||||
msg, newly, err := h.Chat.RecallMessage(claims.ID, roomID, uint(mid), oversee)
|
||||
if err != nil {
|
||||
c.JSON(chatErrToStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
h.broadcastChatRecalled(roomID, msg)
|
||||
h.broadcastChatRecalled(roomID, msg, newly)
|
||||
// 监管者保留正文供界面展示;撤回者本人保留正文供「重新编辑」;其余不回传
|
||||
if !oversee && msg.SenderID != claims.ID {
|
||||
msg.Content = ""
|
||||
@@ -504,19 +504,19 @@ func (h *Handlers) AdminRecallChatMessage(c *gin.Context) {
|
||||
}
|
||||
oversee := h.chatOversee(claims.ID)
|
||||
// 非监管 staff:仅允许撤回自己发送的(与前台一致)
|
||||
msg, err := h.Chat.RecallMessage(claims.ID, uint(roomID), uint(mid), oversee)
|
||||
msg, newly, err := h.Chat.RecallMessage(claims.ID, uint(roomID), uint(mid), oversee)
|
||||
if err != nil {
|
||||
c.JSON(chatErrToStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
h.broadcastChatRecalled(uint(roomID), msg)
|
||||
h.broadcastChatRecalled(uint(roomID), msg, newly)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"ok": true, "id": msg.ID, "room_id": roomID, "recalled_at": msg.RecalledAt,
|
||||
})
|
||||
}
|
||||
|
||||
// broadcastChatRecalled 会话内撤回与管理端撤回共用:先落库再推 chat:{roomID}
|
||||
func (h *Handlers) broadcastChatRecalled(roomID uint, msg *model.ChatMessage) {
|
||||
func (h *Handlers) broadcastChatRecalled(roomID uint, msg *model.ChatMessage, newly bool) {
|
||||
if h.Hub == nil || msg == nil {
|
||||
return
|
||||
}
|
||||
@@ -547,4 +547,35 @@ func (h *Handlers) broadcastChatRecalled(roomID uint, msg *model.ChatMessage) {
|
||||
h.Hub.BroadcastUser(uid, env)
|
||||
}
|
||||
}
|
||||
// 发送时已给未读者 +1;撤回后必须回退,否则角标会停在一条已撤回的「新消息」上
|
||||
if newly {
|
||||
h.broadcastChatUnreadDecrement(roomID, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// broadcastChatUnreadDecrement 与发信对称:给除发送者外的全体成员推 delta:-1。
|
||||
// 不能按 last_read 过滤——发信时 +1 也是全员推的;另一标签页已读后首页角标仍需回退。
|
||||
func (h *Handlers) broadcastChatUnreadDecrement(roomID uint, msg *model.ChatMessage) {
|
||||
if h.Hub == nil || h.Chat == nil || msg == nil {
|
||||
return
|
||||
}
|
||||
ids, err := h.Chat.ListMemberIDs(roomID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
env := realtime.Envelope{
|
||||
Type: realtime.EventChatUnread,
|
||||
Data: gin.H{
|
||||
"room_id": roomID,
|
||||
"message_id": msg.ID,
|
||||
"sender_id": msg.SenderID,
|
||||
"delta": -1,
|
||||
},
|
||||
}
|
||||
for _, uid := range ids {
|
||||
if uid == msg.SenderID {
|
||||
continue
|
||||
}
|
||||
h.Hub.BroadcastUser(uid, env)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,30 +918,31 @@ func (s *ChatService) ListMessages(userID, roomID, beforeID uint, size int, over
|
||||
return msgs, hasMore, nil
|
||||
}
|
||||
|
||||
// RecallMessage 撤回消息:群主可撤自己的;监管者可撤任意;标记 RecalledAt,正文保留供审计
|
||||
func (s *ChatService) RecallMessage(operatorID, roomID, messageID uint, oversee bool) (*model.ChatMessage, error) {
|
||||
// RecallMessage 撤回消息:群主可撤自己的;监管者可撤任意;标记 RecalledAt,正文保留供审计。
|
||||
// newly=true 表示本次刚打上撤回标记(已撤回的幂等调用为 false,避免重复回退角标)。
|
||||
func (s *ChatService) RecallMessage(operatorID, roomID, messageID uint, oversee bool) (*model.ChatMessage, bool, error) {
|
||||
if _, err := s.getRoom(roomID); err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
var msg model.ChatMessage
|
||||
if err := s.db.Where("id = ? AND room_id = ?", messageID, roomID).First(&msg).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrChatMsgNotFound
|
||||
return nil, false, ErrChatMsgNotFound
|
||||
}
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
if msg.RecalledAt != nil {
|
||||
return &msg, nil // 幂等
|
||||
return &msg, false, nil // 幂等
|
||||
}
|
||||
if !oversee {
|
||||
m, err := s.membership(s.db, roomID, operatorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
// 群主/成员仅可撤回自己的消息;监管者可撤任意
|
||||
if msg.SenderID != operatorID {
|
||||
_ = m // 成员校验已通过
|
||||
return nil, ErrChatRecallDenied
|
||||
return nil, false, ErrChatRecallDenied
|
||||
}
|
||||
}
|
||||
now := time.Now()
|
||||
@@ -950,12 +951,12 @@ func (s *ChatService) RecallMessage(operatorID, roomID, messageID uint, oversee
|
||||
"recalled_at": now,
|
||||
"recalled_by": operatorID,
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
msg.RecalledAt = &now
|
||||
msg.RecalledBy = operatorID
|
||||
_ = s.db.Preload("Sender").Preload("Recaller").First(&msg, msg.ID)
|
||||
return &msg, nil
|
||||
return &msg, true, nil
|
||||
}
|
||||
|
||||
// AdminChatParticipant 管理端私聊双方摘要
|
||||
|
||||
Reference in New Issue
Block a user