349 lines
13 KiB
Go
349 lines
13 KiB
Go
// 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
|
||
}
|