// Package dao 提供 IM 模块的数据库访问操作 package dao import ( "context" "time" "github.com/echochat/backend/app/constants" "github.com/echochat/backend/app/im/model" "github.com/echochat/backend/pkg/logs" "go.uber.org/zap" "gorm.io/gorm" ) // ConversationDAO 会话数据访问对象 type ConversationDAO struct { db *gorm.DB } // NewConversationDAO 创建 ConversationDAO 实例 func NewConversationDAO(db *gorm.DB) *ConversationDAO { return &ConversationDAO{db: db} } // FindPrivateConversation 查找两人之间的单聊会话 // 通过 JOIN im_conversation_members 判断是否已存在,避免重复创建 func (d *ConversationDAO) FindPrivateConversation(ctx context.Context, userID, targetUserID int64) (*model.Conversation, error) { funcName := "dao.conversation_dao.FindPrivateConversation" logs.Debug(ctx, funcName, "查找单聊会话", zap.Int64("user_id", userID), zap.Int64("target_user_id", targetUserID)) var conv model.Conversation err := d.db.WithContext(ctx). Raw(`SELECT c.* FROM im_conversations c JOIN im_conversation_members cm1 ON cm1.conversation_id = c.id JOIN im_conversation_members cm2 ON cm2.conversation_id = c.id WHERE cm1.user_id = ? AND cm2.user_id = ? AND c.type = ? LIMIT 1`, userID, targetUserID, constants.ConversationTypePrivate). Scan(&conv).Error if err != nil { logs.Error(ctx, funcName, "查找单聊会话失败", zap.Error(err)) return nil, err } if conv.ID == 0 { return nil, nil } return &conv, nil } // CreateWithMembers 在事务中创建会话及其成员记录 func (d *ConversationDAO) CreateWithMembers(ctx context.Context, conv *model.Conversation, memberIDs []int64) error { funcName := "dao.conversation_dao.CreateWithMembers" logs.Info(ctx, funcName, "创建会话及成员", zap.Int("type", conv.Type), zap.Int("member_count", len(memberIDs))) return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Create(conv).Error; err != nil { logs.Error(ctx, funcName, "创建会话失败", zap.Error(err)) return err } for _, uid := range memberIDs { member := &model.ConversationMember{ ConversationID: conv.ID, UserID: uid, } if err := tx.Create(member).Error; err != nil { logs.Error(ctx, funcName, "创建会话成员失败", zap.Int64("conversation_id", conv.ID), zap.Int64("user_id", uid), zap.Error(err)) return err } } return nil }) } // GetUserConversations 获取用户的会话列表(排除已软删除的) // 返回会话基本信息、该用户的成员视图(未读数、置顶等)和单聊对方用户 ID func (d *ConversationDAO) GetUserConversations(ctx context.Context, userID int64) ([]ConversationWithMember, error) { funcName := "dao.conversation_dao.GetUserConversations" logs.Debug(ctx, funcName, "查询用户会话列表", zap.Int64("user_id", userID)) var results []ConversationWithMember err := d.db.WithContext(ctx). Raw(`SELECT c.id, c.type, c.last_message_id, c.last_msg_content, c.last_msg_time, c.last_msg_sender_id, cm.is_pinned, cm.unread_count, cm.clear_before_msg_id, cm.is_do_not_disturb, cm.at_me_count, COALESCE(peer.user_id, 0) AS peer_user_id FROM im_conversations c JOIN im_conversation_members cm ON cm.conversation_id = c.id AND cm.user_id = ? LEFT JOIN im_conversation_members peer ON peer.conversation_id = c.id AND peer.user_id != ? AND c.type = ? WHERE cm.is_deleted = false ORDER BY cm.is_pinned DESC, c.last_msg_time DESC NULLS LAST`, userID, userID, constants.ConversationTypePrivate). Scan(&results).Error if err != nil { logs.Error(ctx, funcName, "查询用户会话列表失败", zap.Error(err)) } return results, err } // ConversationWithMember 会话列表查询结果(JOIN 成员表 + LEFT JOIN 对方成员) type ConversationWithMember struct { ID int64 `json:"id"` Type int `json:"type"` LastMessageID *int64 `json:"last_message_id"` LastMsgContent string `json:"last_msg_content"` LastMsgTime *time.Time `json:"last_msg_time"` LastMsgSenderID *int64 `json:"last_msg_sender_id"` IsPinned bool `json:"is_pinned"` UnreadCount int `json:"unread_count"` ClearBeforeMsgID int64 `json:"clear_before_msg_id"` IsDoNotDisturb bool `json:"is_do_not_disturb"` AtMeCount int `json:"at_me_count"` PeerUserID int64 `json:"peer_user_id"` } // GetMember 获取指定会话中指定用户的成员记录 func (d *ConversationDAO) GetMember(ctx context.Context, conversationID, userID int64) (*model.ConversationMember, error) { funcName := "dao.conversation_dao.GetMember" logs.Debug(ctx, funcName, "查询会话成员", zap.Int64("conversation_id", conversationID), zap.Int64("user_id", userID)) var member model.ConversationMember err := d.db.WithContext(ctx). Where("conversation_id = ? AND user_id = ?", conversationID, userID). First(&member).Error if err != nil { return nil, err } return &member, nil } // GetPeerUserID 获取单聊会话中对方的用户 ID func (d *ConversationDAO) GetPeerUserID(ctx context.Context, conversationID, userID int64) (int64, error) { funcName := "dao.conversation_dao.GetPeerUserID" logs.Debug(ctx, funcName, "查询对方用户 ID", zap.Int64("conversation_id", conversationID), zap.Int64("user_id", userID)) var peerID int64 err := d.db.WithContext(ctx). Model(&model.ConversationMember{}). Select("user_id"). Where("conversation_id = ? AND user_id != ?", conversationID, userID). Scan(&peerID).Error return peerID, err } // GetConversationMemberIDs 获取会话的所有成员 ID func (d *ConversationDAO) GetConversationMemberIDs(ctx context.Context, conversationID int64) ([]int64, error) { var ids []int64 err := d.db.WithContext(ctx). Model(&model.ConversationMember{}). Where("conversation_id = ?", conversationID). Pluck("user_id", &ids).Error 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" logs.Info(ctx, funcName, "更新置顶状态", zap.Int64("conversation_id", conversationID), zap.Int64("user_id", userID), zap.Bool("is_pinned", isPinned)) return d.db.WithContext(ctx). Model(&model.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, userID). Update("is_pinned", isPinned).Error } // SoftDeleteMember 软删除会话(仅影响当前用户视图,不影响对方) func (d *ConversationDAO) SoftDeleteMember(ctx context.Context, conversationID, userID int64) error { funcName := "dao.conversation_dao.SoftDeleteMember" logs.Info(ctx, funcName, "软删除会话", zap.Int64("conversation_id", conversationID), zap.Int64("user_id", userID)) return d.db.WithContext(ctx). Model(&model.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, userID). Updates(map[string]interface{}{ "is_deleted": true, "unread_count": 0, }).Error } // RestoreMember 恢复已软删除的会话成员(收到新消息时自动恢复) func (d *ConversationDAO) RestoreMember(ctx context.Context, conversationID, userID int64) error { return d.db.WithContext(ctx). Model(&model.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, userID). Update("is_deleted", false).Error } // UpdateLastMessage 更新会话的最后消息信息(冗余字段,避免列表查询 JOIN) func (d *ConversationDAO) UpdateLastMessage(ctx context.Context, conversationID int64, msgID int64, content string, senderID int64, msgTime time.Time) error { funcName := "dao.conversation_dao.UpdateLastMessage" logs.Debug(ctx, funcName, "更新最后消息", zap.Int64("conversation_id", conversationID), zap.Int64("message_id", msgID)) return d.db.WithContext(ctx). Model(&model.Conversation{}). Where("id = ?", conversationID). Updates(map[string]interface{}{ "last_message_id": msgID, "last_msg_content": content, "last_msg_time": msgTime, "last_msg_sender_id": senderID, }).Error } // IncrementUnread 将指定成员的未读消息数 +1 func (d *ConversationDAO) IncrementUnread(ctx context.Context, conversationID, userID int64) error { return d.db.WithContext(ctx). Model(&model.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, userID). UpdateColumn("unread_count", gorm.Expr("unread_count + 1")).Error } // IncrementAtMeCount 将指定成员的 @提醒计数 +1 func (d *ConversationDAO) IncrementAtMeCount(ctx context.Context, conversationID, userID int64) error { return d.db.WithContext(ctx). Model(&model.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, userID). UpdateColumn("at_me_count", gorm.Expr("at_me_count + 1")).Error } // ClearAtMeCount 清零指定成员的 @提醒计数 func (d *ConversationDAO) ClearAtMeCount(ctx context.Context, conversationID, userID int64) error { return d.db.WithContext(ctx). Model(&model.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, userID). Update("at_me_count", 0).Error } // GetMemberDNDMap 批量获取会话成员的免打扰状态(返回 userID → isDoNotDisturb 的映射) func (d *ConversationDAO) GetMemberDNDMap(ctx context.Context, conversationID int64) (map[int64]bool, error) { type memberDND struct { UserID int64 `json:"user_id"` IsDoNotDisturb bool `json:"is_do_not_disturb"` } var members []memberDND err := d.db.WithContext(ctx). Model(&model.ConversationMember{}). Select("user_id, is_do_not_disturb"). Where("conversation_id = ?", conversationID). Scan(&members).Error if err != nil { return nil, err } result := make(map[int64]bool, len(members)) for _, m := range members { result[m.UserID] = m.IsDoNotDisturb } 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). Model(&model.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, userID). Updates(map[string]interface{}{ "unread_count": 0, "last_read_msg_id": lastMsgID, }).Error } // GetUnreadConversations 获取有未读消息的会话列表(用于离线消息推送) func (d *ConversationDAO) GetUnreadConversations(ctx context.Context, userID int64) ([]ConversationWithMember, error) { funcName := "dao.conversation_dao.GetUnreadConversations" logs.Debug(ctx, funcName, "查询未读会话", zap.Int64("user_id", userID)) var results []ConversationWithMember err := d.db.WithContext(ctx). Raw(`SELECT c.id, c.type, c.last_msg_content, c.last_msg_time, c.last_msg_sender_id, cm.is_pinned, cm.unread_count FROM im_conversations c JOIN im_conversation_members cm ON cm.conversation_id = c.id WHERE cm.user_id = ? AND cm.is_deleted = false AND cm.unread_count > 0 ORDER BY c.last_msg_time DESC`, userID). Scan(&results).Error if err != nil { logs.Error(ctx, funcName, "查询未读会话失败", zap.Error(err)) } return results, err } // UpdateClearBefore 更新用户的清空记录截止消息 ID(个人视图操作,不影响对方) func (d *ConversationDAO) UpdateClearBefore(ctx context.Context, conversationID, userID, lastMsgID int64) error { funcName := "dao.conversation_dao.UpdateClearBefore" logs.Info(ctx, funcName, "更新清空记录截止 ID", zap.Int64("conversation_id", conversationID), zap.Int64("user_id", userID), zap.Int64("last_msg_id", lastMsgID)) return d.db.WithContext(ctx). Model(&model.ConversationMember{}). Where("conversation_id = ? AND user_id = ?", conversationID, userID). Updates(map[string]interface{}{ "clear_before_msg_id": lastMsgID, "unread_count": 0, }).Error } // GetByID 根据 ID 获取会话 func (d *ConversationDAO) GetByID(ctx context.Context, id int64) (*model.Conversation, error) { var conv model.Conversation err := d.db.WithContext(ctx).First(&conv, id).Error if err != nil { return nil, err } return &conv, nil }