package dao import ( "context" "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" ) // MessageDAO 消息数据访问对象 type MessageDAO struct { db *gorm.DB } // NewMessageDAO 创建 MessageDAO 实例 func NewMessageDAO(db *gorm.DB) *MessageDAO { return &MessageDAO{db: db} } // Create 写入一条消息 func (d *MessageDAO) Create(ctx context.Context, msg *model.Message) error { funcName := "dao.message_dao.Create" logs.Debug(ctx, funcName, "写入消息", zap.Int64("conversation_id", msg.ConversationID), zap.Int64("sender_id", msg.SenderID)) err := d.db.WithContext(ctx).Create(msg).Error if err != nil { logs.Error(ctx, funcName, "写入消息失败", zap.Error(err)) } return err } // GetByID 根据 ID 查询单条消息 func (d *MessageDAO) GetByID(ctx context.Context, id int64) (*model.Message, error) { var msg model.Message err := d.db.WithContext(ctx).First(&msg, id).Error if err != nil { return nil, err } return &msg, nil } // GetByConversation 获取会话的历史消息(游标分页,按 ID 降序) // beforeID > 0 时作为游标,查询 ID 小于 beforeID 的消息 // clearBeforeMsgID > 0 时过滤掉用户已清空的消息(个人视图) func (d *MessageDAO) GetByConversation(ctx context.Context, conversationID int64, beforeID int64, clearBeforeMsgID int64, limit int) ([]model.Message, error) { funcName := "dao.message_dao.GetByConversation" logs.Debug(ctx, funcName, "查询历史消息", zap.Int64("conversation_id", conversationID), zap.Int64("before_id", beforeID), zap.Int64("clear_before_msg_id", clearBeforeMsgID), zap.Int("limit", limit)) query := d.db.WithContext(ctx). Where("conversation_id = ? AND status != ?", conversationID, constants.MessageStatusDeleted) if beforeID > 0 { query = query.Where("id < ?", beforeID) } if clearBeforeMsgID > 0 { query = query.Where("id > ?", clearBeforeMsgID) } var messages []model.Message err := query.Order("id DESC").Limit(limit).Find(&messages).Error if err != nil { logs.Error(ctx, funcName, "查询历史消息失败", zap.Error(err)) } return messages, err } // UpdateStatus 更新消息状态(撤回/删除) func (d *MessageDAO) UpdateStatus(ctx context.Context, id int64, status int) error { funcName := "dao.message_dao.UpdateStatus" logs.Info(ctx, funcName, "更新消息状态", zap.Int64("message_id", id), zap.Int("status", status)) return d.db.WithContext(ctx). Model(&model.Message{}). Where("id = ?", id). Update("status", status).Error } // SearchMessages 全局消息搜索(按关键词匹配,仅搜索用户所在会话的消息) // 返回匹配的消息列表(已 JOIN 成员表确保权限) func (d *MessageDAO) SearchMessages(ctx context.Context, userID int64, keyword string, limit int) ([]MessageSearchResult, error) { funcName := "dao.message_dao.SearchMessages" logs.Debug(ctx, funcName, "全局消息搜索", zap.Int64("user_id", userID), zap.String("keyword", keyword)) var results []MessageSearchResult err := d.db.WithContext(ctx). Raw(`SELECT m.id, m.conversation_id, m.sender_id, m.content, m.created_at FROM im_messages m JOIN im_conversation_members cm ON cm.conversation_id = m.conversation_id WHERE cm.user_id = ? AND cm.is_deleted = false AND m.status = ? AND to_tsvector('simple', m.content) @@ plainto_tsquery('simple', ?) ORDER BY m.created_at DESC LIMIT ?`, userID, constants.MessageStatusNormal, keyword, limit). Scan(&results).Error if err != nil { logs.Error(ctx, funcName, "全局消息搜索失败", zap.Error(err)) } return results, err } // MessageSearchResult 搜索结果单条记录 type MessageSearchResult struct { ID int64 `json:"id"` ConversationID int64 `json:"conversation_id"` SenderID int64 `json:"sender_id"` Content string `json:"content"` CreatedAt string `json:"created_at"` } // FindByClientMsgID 根据客户端消息 ID 查询(幂等去重用) func (d *MessageDAO) FindByClientMsgID(ctx context.Context, conversationID int64, clientMsgID string) (*model.Message, error) { if clientMsgID == "" { return nil, nil } var msg model.Message err := d.db.WithContext(ctx). Where("conversation_id = ? AND client_msg_id = ?", conversationID, clientMsgID). First(&msg).Error if err == gorm.ErrRecordNotFound { return nil, nil } if err != nil { return nil, err } return &msg, nil } // GetLatestMessageID 获取会话中最新一条消息的 ID(用于标记已读) func (d *MessageDAO) GetLatestMessageID(ctx context.Context, conversationID int64) (int64, error) { var id int64 err := d.db.WithContext(ctx). Model(&model.Message{}). Select("COALESCE(MAX(id), 0)"). Where("conversation_id = ?", conversationID). Scan(&id).Error return id, err }