Files
EchoChat/backend/go-service/app/im/dao/message_dao.go
bujinyuan e091fbccb3 feat(im): Task 2 - IM DAO 层(会话 + 消息)
ConversationDAO:
- FindPrivateConversation: 单聊会话去重查询
- CreateWithMembers: 事务创建会话+成员
- GetUserConversations: 会话列表(含未读/置顶)
- UpdateLastMessage / IncrementUnread / ClearUnread
- SoftDeleteMember / RestoreMember / UpdateMemberPinned
- GetUnreadConversations: 离线消息推送用

MessageDAO:
- Create / GetByID / GetByConversation(游标分页)
- UpdateStatus(撤回) / DeleteByConversation(清空)
- SearchMessages(全局搜索,JOIN 权限校验)
- FindByClientMsgID(幂等去重)

Made-with: Cursor
2026-03-03 10:44:34 +08:00

158 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 的消息
// beforeID = 0 时查询最新的 limit 条消息
func (d *MessageDAO) GetByConversation(ctx context.Context, conversationID int64, beforeID 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.Int("limit", limit))
query := d.db.WithContext(ctx).
Where("conversation_id = ? AND status != ?", conversationID, constants.MessageStatusDeleted)
if beforeID > 0 {
query = query.Where("id < ?", beforeID)
}
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
}
// DeleteByConversation 软删除会话中所有消息(标记 status=3
func (d *MessageDAO) DeleteByConversation(ctx context.Context, conversationID int64) error {
funcName := "dao.message_dao.DeleteByConversation"
logs.Info(ctx, funcName, "清空会话消息",
zap.Int64("conversation_id", conversationID))
return d.db.WithContext(ctx).
Model(&model.Message{}).
Where("conversation_id = ? AND status = ?", conversationID, constants.MessageStatusNormal).
Update("status", constants.MessageStatusDeleted).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 m.content LIKE ?
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
}