feat:群聊问题修复+相关项目文档更新

This commit is contained in:
bujinyuan
2026-03-05 09:24:49 +08:00
parent 4f55ef6de0
commit 8ee870636c
29 changed files with 1155 additions and 225 deletions

View File

@@ -63,6 +63,35 @@ func (ctl *IMController) GetHistoryMessages(c *gin.Context) {
utils.ResponseOK(c, result)
}
// SetDoNotDisturb 设置/取消会话消息免打扰
// PUT /api/v1/im/conversations/:id/dnd
func (ctl *IMController) SetDoNotDisturb(c *gin.Context) {
ctx := c.Request.Context()
userID, ok := middleware.GetCurrentUserID(c)
if !ok {
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
return
}
convID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
utils.ResponseBadRequest(c, "会话 ID 格式错误")
return
}
var req dto.SetDoNotDisturbRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.ResponseBadRequest(c, "请求参数错误: "+err.Error())
return
}
if err := ctl.imService.SetDoNotDisturb(ctx, userID, convID, req.IsDoNotDisturb); err != nil {
ctl.handleError(c, err, "设置免打扰失败")
return
}
utils.ResponseOK(c, nil)
}
// PinConversation 置顶/取消置顶会话
// PUT /api/v1/im/conversations/:id/pin
func (ctl *IMController) PinConversation(c *gin.Context) {

View File

@@ -159,6 +159,29 @@ func (d *ConversationDAO) GetConversationMemberIDs(ctx context.Context, conversa
return ids, err
}
// GetMemberNicknameMap 批量获取会话成员的群内昵称
// 返回 map[user_id]nickname未设置昵称的不在 map 中
func (d *ConversationDAO) GetMemberNicknameMap(ctx context.Context, conversationID int64) (map[int64]string, error) {
type row struct {
UserID int64 `gorm:"column:user_id"`
Nickname string `gorm:"column:nickname"`
}
var rows []row
err := d.db.WithContext(ctx).
Model(&model.ConversationMember{}).
Select("user_id, nickname").
Where("conversation_id = ? AND nickname != ''", conversationID).
Find(&rows).Error
if err != nil {
return nil, err
}
result := make(map[int64]string, len(rows))
for _, r := range rows {
result[r.UserID] = r.Nickname
}
return result, nil
}
// UpdateMemberPinned 更新会话成员的置顶状态
func (d *ConversationDAO) UpdateMemberPinned(ctx context.Context, conversationID, userID int64, isPinned bool) error {
funcName := "dao.conversation_dao.UpdateMemberPinned"
@@ -257,6 +280,14 @@ func (d *ConversationDAO) GetMemberDNDMap(ctx context.Context, conversationID in
return result, nil
}
// UpdateMemberDND 设置/取消指定成员的消息免打扰
func (d *ConversationDAO) UpdateMemberDND(ctx context.Context, conversationID, userID int64, isDoNotDisturb bool) error {
return d.db.WithContext(ctx).
Model(&model.ConversationMember{}).
Where("conversation_id = ? AND user_id = ?", conversationID, userID).
Update("is_do_not_disturb", isDoNotDisturb).Error
}
// ClearUnread 清零指定成员的未读消息数
func (d *ConversationDAO) ClearUnread(ctx context.Context, conversationID, userID int64, lastMsgID int64) error {
return d.db.WithContext(ctx).

View File

@@ -14,6 +14,7 @@ func RegisterRoutes(r *gin.Engine, ctrl *controller.IMController, jwtAuth gin.Ha
// 会话管理
authed.GET("/conversations", ctrl.GetConversations)
authed.PUT("/conversations/:id/pin", ctrl.PinConversation)
authed.PUT("/conversations/:id/dnd", ctrl.SetDoNotDisturb)
authed.DELETE("/conversations/:id", ctrl.DeleteConversation)
authed.DELETE("/conversations/:id/messages", ctrl.ClearHistory)

View File

@@ -525,7 +525,7 @@ func (s *IMService) GetHistoryMessages(ctx context.Context, userID int64, req *d
return &dto.HistoryMessageResponse{List: list, HasMore: hasMore}, nil
}
// MarkRead 标记会话已读(清零未读 + 更新 Redis 总未读数)
// MarkRead 标记会话已读(清零未读 + 更新 Redis 总未读数 + 推送已读 ACK 给对方
func (s *IMService) MarkRead(ctx context.Context, userID int64, conversationID int64) error {
funcName := "service.im_service.MarkRead"
logs.Info(ctx, funcName, "标记已读",
@@ -536,22 +536,35 @@ func (s *IMService) MarkRead(ctx context.Context, userID int64, conversationID i
return ErrNotMember
}
if member.UnreadCount == 0 {
return nil
}
latestMsgID, err := s.msgDAO.GetLatestMessageID(ctx, conversationID)
if err != nil {
logs.Error(ctx, funcName, "获取最新消息 ID 失败", zap.Error(err))
return err
}
if err := s.convDAO.ClearUnread(ctx, conversationID, userID, latestMsgID); err != nil {
logs.Error(ctx, funcName, "清零未读失败", zap.Error(err))
return err
if member.UnreadCount > 0 {
if err := s.convDAO.ClearUnread(ctx, conversationID, userID, latestMsgID); err != nil {
logs.Error(ctx, funcName, "清零未读失败", zap.Error(err))
return err
}
s.decrementTotalUnread(ctx, userID, member.UnreadCount)
}
memberIDs, err := s.convDAO.GetConversationMemberIDs(ctx, conversationID)
if err != nil {
logs.Error(ctx, funcName, "获取会话成员失败", zap.Error(err))
return nil
}
for _, peerID := range memberIDs {
if peerID != userID {
s.pushToUser(ctx, peerID, "im.message.read.ack", map[string]interface{}{
"conversation_id": conversationID,
"reader_id": userID,
"last_read_msg_id": latestMsgID,
})
}
}
s.decrementTotalUnread(ctx, userID, member.UnreadCount)
return nil
}
@@ -658,6 +671,12 @@ func (s *IMService) GetMessageReadDetail(ctx context.Context, userID int64, mess
}
}
nicknameMap, nErr := s.convDAO.GetMemberNicknameMap(ctx, msg.ConversationID)
if nErr != nil {
logs.Error(ctx, funcName, "获取成员群昵称失败", zap.Error(nErr))
nicknameMap = make(map[int64]string)
}
var readList, unreadList []dto.MessageReadDetailDTO
for _, uid := range allUserIDs {
item := dto.MessageReadDetailDTO{
@@ -667,6 +686,9 @@ func (s *IMService) GetMessageReadDetail(ctx context.Context, userID int64, mess
item.UserNickname = brief.Nickname
item.UserAvatar = brief.Avatar
}
if gn, ok := nicknameMap[uid]; ok {
item.GroupNickname = gn
}
if readUserIDs[uid] {
readList = append(readList, item)
} else {
@@ -682,6 +704,20 @@ func (s *IMService) GetMessageReadDetail(ctx context.Context, userID int64, mess
}, nil
}
// SetDoNotDisturb 设置/取消会话消息免打扰
func (s *IMService) SetDoNotDisturb(ctx context.Context, userID int64, conversationID int64, isDoNotDisturb bool) error {
funcName := "service.im_service.SetDoNotDisturb"
logs.Info(ctx, funcName, "更新免打扰状态",
zap.Int64("user_id", userID), zap.Int64("conversation_id", conversationID), zap.Bool("is_do_not_disturb", isDoNotDisturb))
member, err := s.convDAO.GetMember(ctx, conversationID, userID)
if err != nil || member == nil {
return ErrNotMember
}
return s.convDAO.UpdateMemberDND(ctx, conversationID, userID, isDoNotDisturb)
}
// PinConversation 置顶/取消置顶会话
func (s *IMService) PinConversation(ctx context.Context, userID int64, conversationID int64, isPinned bool) error {
funcName := "service.im_service.PinConversation"