OpenIM 源码深度解析系列(九):单聊(第三阶段)消息接收流程

405 阅读43分钟

单聊(第三阶段)消息接收流程

概述

第三阶段负责在设备端接收并处理服务端推送的消息。本阶段采用三协程架构确保消息处理的高效性和稳定性。

三协程架构设计原理

OpenIM采用三个专门的协程来协调处理消息接收事件:

  1. 消息读取协程(readPump)

    • 专门负责从WebSocket连接中读取消息
    • 不进行复杂的业务逻辑处理,确保读取效率
    • 将消息快速分发到对应的处理通道
  2. 消息同步协程(DoListener)

    • 专门负责处理消息同步逻辑
    • 处理连续性检查、缺失消息补偿等复杂业务
    • 避免阻塞消息读取流程
  3. 会话协程(ConsumeConversationEventLoop)

    • 专门负责处理会话相关的所有事件
    • 处理消息存储、会话更新、UI通知等业务
    • 与消息同步协程通过事件队列解耦

三协程分工协作的优势

  • 完全解耦读取、同步和处理:避免任何环节阻塞整个流程
  • 提高并发性能:三个协程可以并行工作,最大化处理效率
  • 增强稳定性:任何一个协程异常不会影响其他协程
  • 便于扩展:可以独立优化每个环节的处理逻辑
  • 清晰的职责边界:每个协程专注于特定的功能领域

核心流程图

设备端消息接收核心流程图

graph TD
    A[服务端推送消息] --> B[readPump读取协程]
    B --> C[handleMessage解析消息]
    C --> D[发送到消息同步通道]
    
    D --> E[DoListener同步协程]
    E --> F[连续性检查]
    
    F --> G{消息是否连续}
    G -->|连续| H[直接处理消息]
    G -->|不连续| I[拉取缺失消息]
    I --> H
    
    H --> J[发送到会话事件队列]
    
    J --> K[会话协程处理]
    K --> L[doMsgNew核心处理]
    
    L --> M{发送者判断}
    M -->|自己发送| N[处理自己的消息]
    M -->|他人发送| O[处理他人的消息]
    
    N --> P[消息去重和状态更新]
    O --> Q[创建会话和未读计数]
    
    P --> R[批量数据库操作]
    Q --> R
    
    R --> S[触发UI事件通知]
    S --> T[用户看到新消息]
    
    style A fill:#e3f2fd
    style B fill:#f3e5f5
    style E fill:#e8f5e8
    style K fill:#fff3e0
    style L fill:#ffebee
    style T fill:#c8e6c9

关键技术点

  1. 消息连续性保证:通过序列号检查确保消息完整性
  2. 异常消息处理:handleExceptionMessages处理各种边界情况
  3. 会话状态同步:maxSeqRecorder和未读数管理
  4. 隐藏会话复活:保持用户设置的同时恢复会话显示
  5. 批量数据库操作:提高存储性能和事务完整性
  6. 事件驱动通知:区分前台后台,分别触发合适的通知方式

第一步:长连接读取推送消息

长连接管理器核心结构

负责维护WebSocket连接,提供消息读取的基础设施。

// LongConnMgr 长连接管理器
// 路径:openim-sdk-core/internal/interaction/long_conn_mgr.go:85-125
type LongConnMgr struct {
	// 连接状态相关
	w          sync.Mutex // 连接状态互斥锁,保护connStatus字段
	connStatus int        // 连接状态:DefaultNotConnect/Closed/Connecting/Connected

	// 核心连接组件
	conn     LongConn                                   // 长连接接口,可以是TCP或WebSocket实现
	listener func() open_im_sdk_callback.OnConnListener // 连接事件监听器回调函数

	// 消息通道(三协程架构的核心)
	send               chan Message          // 缓冲的出站消息通道
	pushMsgAndMaxSeqCh chan common.Cmd2Value // 推送消息通道,分发给DoListener协程
	conversationCh     chan common.Cmd2Value // 会话相关消息通道,分发给会话协程
	loginMgrCh         chan common.Cmd2Value // 登录管理消息通道

	// 消息处理组件
	IsCompression     bool              // 是否启用消息压缩
	Syncer            *WsRespAsyn       // WebSocket响应异步处理器
	encoder           Encoder           // 消息编码器
	compressor        Compressor        // 消息压缩器
	reconnectStrategy ReconnectStrategy // 重连策略
}

readPump消息读取主循环

消息读取协程持续监听WebSocket连接,接收服务端推送的消息。

// readPump 消息读取主循环(消息读取协程)
// 路径:openim-sdk-core/internal/interaction/long_conn_mgr.go:242-341
func (c *LongConnMgr) readPump(ctx context.Context, fgCtx context.Context) {
	// 设置消息处理回调
	c.conn.SetPingHandler(c.pingHandler)
	c.conn.SetPongHandler(c.pongHandler)

	// 持续读取消息循环
	for {
		select {
		case <-ctx.Done():
			return
		default:
			// 设置读取超时时间,防止长时间阻塞
			if err := c.conn.SetReadDeadline(readTimeout); err != nil {
				log.ZError(ctx, "SetReadDeadline failed", err)
				c.closedErr = err
				return
			}

			// 从WebSocket连接读取消息
			_, message, err := c.conn.ReadMessage()
			if err != nil {
				log.ZError(ctx, "ReadMessage failed", err)
				c.closedErr = err
				return
			}

			// 立即处理消息,不进行复杂业务逻辑
			err = c.handleMessage(message)
			if err != nil {
				log.ZError(ctx, "handleMessage failed", err, "message", string(message))
			}
		}
	}
}

handleMessage消息处理和路由

解析消息并将其分发到对应的处理通道,实现读取和处理的解耦。

// handleMessage 消息处理和路由
// 路径:openim-sdk-core/internal/interaction/long_conn_mgr.go:709-797
func (c *LongConnMgr) handleMessage(message []byte) error {
	// 解压缩消息(如果启用压缩)
	var binaryResp GeneralWsResp
	if c.IsCompression {
		message, err := c.compressor.DecompressWithPool(message)
		if err != nil {
			return errs.WrapMsg(err, "decompression failed")
		}
	}

	// 解码消息
	if err := c.encoder.Decode(message, &binaryResp); err != nil {
		return errs.WrapMsg(err, "decode failed")
	}

	// 根据消息类型进行路由分发
	switch binaryResp.ReqIdentifier {
	case constant.WSPushMsg:
		// 推送消息类型 - 分发给消息同步协程处理
		return c.doPushMsg(ctx, binaryResp)
	case constant.WSKickOnlineMsg:
		// 设备踢下线通知
		return c.handleUserOnlineChange(ctx, binaryResp)
	// ... 其他消息类型
	default:
		// 响应类消息,交给异步响应处理器
		return c.Syncer.NotifyResp(ctx, binaryResp)
	}
}

doPushMsg消息分发机制

将推送消息封装后发送到消息同步器的处理通道。

// doPushMsg 消息分发机制
// 路径:openim-sdk-core/internal/interaction/long_conn_mgr.go:1016-1023
func (c *LongConnMgr) doPushMsg(ctx context.Context, wsResp GeneralWsResp) error {
	// 解析推送消息
	var pushMessages sdkws.PushMessages
	if err := proto.Unmarshal(wsResp.Data, &pushMessages); err != nil {
		return errs.WrapMsg(err, "unmarshal push messages failed")
	}

	// 🔄 协程切换点:发送到消息同步器通道,由消息同步协程处理
	c.pushMsgAndMaxSeqCh <- common.Cmd2Value{
		Cmd:   constant.CmdPushMsg,
		Value: &pushMessages,
		Ctx:   ctx,
	}
	return nil
}

第二步:消息同步器处理逻辑

MsgSyncer核心结构

消息同步器负责处理所有的消息同步逻辑,包括连续性检查和缺失消息补偿。

// MsgSyncer 消息同步器结构
// 路径:openim-sdk-core/internal/interaction/msg_sync.go:61-85
type MsgSyncer struct {
	loginUserID string       // 登录用户ID
	longConnMgr *LongConnMgr // 长连接管理器

	// 消息接收和事件队列
	recvCh                 chan common.Cmd2Value // 接收推送消息和最大SEQ号的通道
	conversationEventQueue *common.EventQueue    // 会话事件队列,用于存储和触发会话相关事件

	// 同步状态管理
	syncedMaxSeqs     map[string]int64 // 所有会话ID对应的最大已同步SEQ号映射
	syncedMaxSeqsLock sync.RWMutex     // 保护syncedMaxSeqs映射的读写锁

	// 数据存储和应用状态
	db          db_interface.DataBase // 数据存储接口
	reinstalled bool                  // 标记应用是否被卸载后重新安装

	// 同步控制
	isSyncing     bool       // 标记是否正在进行数据同步
	isSyncingLock sync.Mutex // 同步状态的互斥锁
}

LoadSeq并发初始化序列号状态

应用启动时,并发加载所有会话的最大同步序列号到内存。

// LoadSeq 并发初始化序列号状态
// 路径:openim-sdk-core/internal/interaction/msg_sync.go:124-227
func (m *MsgSyncer) LoadSeq(ctx context.Context) error {
	// 获取所有会话的最大同步序列号
	conversations, err := m.db.GetAllConversations(ctx)
	if err != nil {
		return errs.WrapMsg(err, "GetAllConversations failed")
	}

	// 使用并发goroutine加载序列号,提高初始化性能
	seqCh := make(chan SyncedSeq, len(conversations))
	var wg sync.WaitGroup

	// 并发处理每个会话的序列号加载
	for _, conversation := range conversations {
		wg.Add(1)
		go func(conversationID string) {
			defer wg.Done()
			// 获取会话的最大同步序列号
			maxSeq, err := m.db.GetConversationNormalMsgSeq(ctx, conversationID)
			seqCh <- SyncedSeq{
				ConversationID: conversationID,
				MaxSyncedSeq:   maxSeq,
				Err:            err,
			}
		}(conversation.ConversationID)
	}

	// 等待所有goroutine完成
	go func() {
		wg.Wait()
		close(seqCh)
	}()

	// 收集结果并更新内存状态
	m.syncedMaxSeqs = make(map[string]int64)
	for result := range seqCh {
		if result.Err != nil {
			log.ZError(ctx, "GetConversationNormalMsgSeq failed", result.Err, 
				"conversationID", result.ConversationID)
			continue
		}
		m.syncedMaxSeqs[result.ConversationID] = result.MaxSyncedSeq
	}

	return nil
}

DoListener监听器主循环

消息同步协程持续监听消息同步事件,是三协程架构中的核心处理协程。

// DoListener 监听器主循环(消息同步协程)
// 路径:openim-sdk-core/internal/interaction/msg_sync.go:228-258
func (m *MsgSyncer) DoListener(ctx context.Context) {
	// 持续监听各种同步事件
	for {
		select {
		case <-ctx.Done():
			return
		case cmd := <-m.recvCh:
			// 处理来自消息读取协程的消息和事件
			m.handlePushMsgAndEvent(cmd)
		}
	}
}

handlePushMsgAndEvent事件分发逻辑

根据不同的命令类型,选择相应的处理方法。

// handlePushMsgAndEvent 事件分发逻辑
// 路径:openim-sdk-core/internal/interaction/msg_sync.go:277-327
func (m *MsgSyncer) handlePushMsgAndEvent(cmd common.Cmd2Value) {
	switch cmd.Cmd {
	case constant.CmdConnSuccesss:
		// 连接成功事件:建立连接后立即同步最新消息
		log.ZInfo(cmd.Ctx, "recv long conn mgr connected", "cmd", cmd.Cmd, "value", cmd.Value)
		if m.startSync() {
			// 获取同步锁成功,开始连接后同步
			m.doConnected(cmd.Ctx)
		}

	case constant.CmdWakeUpDataSync:
		// 应用唤醒事件:从后台切换到前台时触发数据同步
		log.ZInfo(cmd.Ctx, "app wake up, start sync msgs", "cmd", cmd.Cmd, "value", cmd.Value)
		if m.startSync() {
			m.doWakeupDataSync(cmd.Ctx)
		}

	case constant.CmdPushMsg:
		// 推送消息事件:处理服务端推送的实时消息
		m.doPushMsg(cmd.Ctx, cmd.Value.(*sdkws.PushMessages))
	}
}

第三步:推送消息处理与连续性检查

doPushMsg处理推送消息

分别处理普通消息和通知消息,确保两种类型的消息都能正确同步。

🔄 推送消息处理中的关键属性变化

  • syncedMaxSeqs更新:内存中维护的各会话最大已同步序列号状态
  • 消息连续性判断:基于MaxSeq进行消息连续性检查
  • 分类处理策略:普通消息和通知消息分别处理,影响不同类型的会话状态
  • 异步触发机制:通过triggerFunc回调异步更新会话和消息状态
// doPushMsg 处理推送消息
// 路径:openim-sdk-core/internal/interaction/msg_sync.go:527-533
func (m *MsgSyncer) doPushMsg(ctx context.Context, push *sdkws.PushMessages) {
	log.ZDebug(ctx, "push msgs", "push", push, "syncedMaxSeqs", m.syncedMaxSeqs)
	// 处理普通会话消息(单聊、群聊)
	m.pushTriggerAndSync(ctx, push.Msgs, m.triggerConversation)
	// 处理通知消息(系统通知、入群通知等)
	m.pushTriggerAndSync(ctx, push.NotificationMsgs, m.triggerNotification)
}

pushTriggerAndSync连续性检查核心算法

这是消息连续性保证的核心方法,确保消息的完整性和顺序性。

🔄 连续性检查中的MaxSeq状态管理

  • 内存MaxSeq更新m.syncedMaxSeqs[conversationID] = lastSeq 实时更新会话最大序列号
  • 连续性判断公式lastSeq == syncedMaxSeqs[conversationID] + len(storageMsgs) 确保无消息丢失
  • 缺失消息检测lastSeq > syncedMaxSeqs[conversationID] 且不连续时触发补偿同步
  • 序列号范围计算[syncedMaxSeqs + 1, lastSeq] 精确定位需要补偿的消息范围
  • 状态同步策略:连续消息立即更新状态,缺失消息等待补偿后统一更新
// pushTriggerAndSync 推送消息的触发和同步处理
// 路径:openim-sdk-core/internal/interaction/msg_sync.go:545-589
func (m *MsgSyncer) pushTriggerAndSync(ctx context.Context, pushMessages map[string]*sdkws.PullMsgs, triggerFunc func(ctx context.Context, msgs map[string]*sdkws.PullMsgs) error) {
	if len(pushMessages) == 0 {
		return
	}
	needSyncSeqMap := make(map[string][2]int64) // 需要同步的序列号范围映射
	var lastSeq int64                           // 当前会话的最后一个序列号
	var storageMsgs []*sdkws.MsgData            // 当前会话的存储消息列表

	// 遍历每个会话的推送消息
	for conversationID, msgs := range pushMessages {
		for _, msg := range msgs.Msgs {
			// 序列号为0的消息(如系统消息)直接触发,不参与连续性检查
			if msg.Seq == 0 {
				_ = triggerFunc(ctx, map[string]*sdkws.PullMsgs{conversationID: {Msgs: []*sdkws.MsgData{msg}}})
				continue
			}
			lastSeq = msg.Seq
			storageMsgs = append(storageMsgs, msg)
		}

		// 检查消息连续性:如果最后一个序列号等于本地最大序列号加上消息数量,说明消息是连续的
		if lastSeq == m.syncedMaxSeqs[conversationID]+int64(len(storageMsgs)) && lastSeq != 0 {
			log.ZDebug(ctx, "trigger msgs", "msgs", storageMsgs)
			// 消息连续,直接触发事件
			_ = triggerFunc(ctx, map[string]*sdkws.PullMsgs{conversationID: {Msgs: storageMsgs}})
			// 更新本地同步状态
			m.syncedMaxSeqs[conversationID] = lastSeq
		} else if lastSeq != 0 && lastSeq > m.syncedMaxSeqs[conversationID] {
			// 消息不连续,需要拉取缺失的消息
			needSyncSeqMap[conversationID] = [2]int64{m.syncedMaxSeqs[conversationID] + 1, lastSeq}
		}
	}
	// 同步缺失的消息
	m.syncAndTriggerMsgs(ctx, needSyncSeqMap, defaultPullNums)
}

连续性检查算法详解

连续性检查的核心公式为:

lastSeq == syncedMaxSeqs[conversationID] + len(storageMsgs)
  • lastSeq:推送消息中的最大序列号
  • syncedMaxSeqs[conversationID]:本地已同步的最大序列号
  • len(storageMsgs):当前推送的消息数量

如果等式成立,说明消息是连续的;否则存在缺失,需要补偿同步。

第四步:处理缺失消息的逻辑

syncAndTriggerMsgs同步缺失消息

当检测到消息不连续时,主动拉取缺失的消息以保证完整性。

🔄 缺失消息补偿中的序列号状态恢复

  • 批量拉取策略:通过pullMsgBySeqRange批量获取缺失的消息段
  • MaxSeq状态恢复m.syncedMaxSeqs[conversationID] = maxSeq 恢复会话最大序列号
  • 消息完整性保证:确保[begin, end]范围内的所有消息都被正确同步
  • 分类触发机制:普通消息和通知消息分别触发不同的处理流程
  • 状态一致性:补偿完成后内存状态与实际消息状态保持一致
// syncAndTriggerMsgs 同步缺失消息
// 路径:openim-sdk-core/internal/interaction/msg_sync.go:788-866
func (m *MsgSyncer) syncAndTriggerMsgs(ctx context.Context, seqMap map[string][2]int64, syncMsgNum int64) error {
	if len(seqMap) == 0 {
		return nil
	}

	// 拉取缺失的消息
	resp, err := m.pullMsgBySeqRange(ctx, seqMap, syncMsgNum)
	if err != nil {
		return errs.WrapMsg(err, "pullMsgBySeqRange failed")
	}

	// 检查并获取最新消息
	messages := make(map[string]*sdkws.PullMsgs)
	for conversationID, pullMsgs := range resp.Msgs {
		messages[conversationID] = pullMsgs
	}
	for conversationID, pullMsgs := range resp.NotificationMsgs {
		messages[conversationID] = pullMsgs
	}

	// 更新同步状态
	for conversationID, seqRange := range seqMap {
		if msgs, ok := messages[conversationID]; ok && len(msgs.Msgs) > 0 {
			// 根据拉取到的消息更新最大同步序列号
			maxSeq := seqRange[1] // 使用请求的结束序列号
			m.syncedMaxSeqs[conversationID] = maxSeq
		}
	}

	// 🔄 协程切换点:分别触发普通消息和通知消息处理
	if len(resp.Msgs) > 0 {
		_ = m.triggerConversation(ctx, resp.Msgs)
	}
	if len(resp.NotificationMsgs) > 0 {
		_ = m.triggerNotification(ctx, resp.NotificationMsgs)
	}

	return nil
}

pullMsgBySeqRange拉取消息实现

向服务端发起消息拉取请求,获取指定序列号范围内的消息。

// pullMsgBySeqRange 拉取消息实现
// 路径:openim-sdk-core/internal/interaction/msg_sync.go:1056-1091
func (m *MsgSyncer) pullMsgBySeqRange(ctx context.Context, seqMap map[string][2]int64, syncMsgNum int64) (resp *sdkws.PullMessageBySeqsResp, err error) {
	// 构建请求参数
	var seqRanges []*sdkws.SeqRange
	for conversationID, seq := range seqMap {
		seqRanges = append(seqRanges, &sdkws.SeqRange{
			ConversationID: conversationID,
			Begin:          seq[0],  // 起始序列号
			End:            seq[1],  // 结束序列号
			Num:            syncMsgNum, // 拉取数量限制
		})
	}

	// 发送拉取请求到服务端
	req := &sdkws.PullMessageBySeqsReq{
		UserID:    m.loginUserID,
		SeqRanges: seqRanges,
		Order:     sdkws.PullOrder_PullOrderAsc, // 升序拉取
	}

	// 等待服务端响应
	err = m.longConnMgr.SendReqWaitResp(ctx, req, constant.PullMsgBySeqList, &resp)
	if err != nil {
		return nil, errs.WrapMsg(err, "SendReqWaitResp failed")
	}

	return resp, nil
}

第五步:triggerConversation消息事件处理详解

doPushMsg之后的triggerConversation处理流程

doPushMsg处理完推送消息和连续性检查后,会调用triggerConversation方法来处理普通会话消息(单聊、群聊),这是消息同步协程向会话协程传递事件的关键步骤。

步骤1:triggerConversation事件封装

// triggerConversation 触发会话消息处理
// 路径:openim-sdk-core/internal/interaction/msg_sync.go:1159-1186
func (m *MsgSyncer) triggerConversation(ctx context.Context, msgs map[string]*sdkws.PullMsgs) error {
    // 🔄 步骤1:基础验证
    if len(msgs) == 0 {
        log.ZWarn(ctx, "msgs is empty", errs.New("msgs is empty"))
        return nil
    }

    // 🔄 步骤2:构造会话事件数据结构
    // CmdNewMsgComeToConversation是会话协程的核心事件载体
    cmd := sdk_struct.CmdNewMsgComeToConversation{
        Msgs:     msgs,               // 按会话ID分组的消息列表
        SyncFlag: constant.MsgSyncFlag, // 同步标志,区分实时消息和历史同步
    }
    
    log.ZDebug(ctx, "trigger conversation", "msgNum", len(msgs), "syncFlag", cmd.SyncFlag)
    
    // 🔄 步骤3:关键协程切换点
    // 从消息同步协程切换到会话协程,通过事件队列异步传递
    return common.DispatchNewMessage(ctx, cmd, m.conversationEventQueue)
}

步骤2:DispatchNewMessage事件分发

// DispatchNewMessage 分发新消息事件到会话协程
// 路径:openim-sdk-core/pkg/common/trigger_channel.go:113-122
func DispatchNewMessage(ctx context.Context, msg sdk_struct.CmdNewMsgComeToConversation, queue *EventQueue) error {
    // 🔄 步骤2.1:构造命令值对象
    cmd := Cmd2Value{
        Cmd:    constant.CmdNewMsgCome,  // 新消息到达命令标识
        Value:  msg,                     // 消息数据载体
        Caller: GetCaller(2),            // 调用者信息,用于调试
        Ctx:    ctx,                     // 上下文传递
    }
    
    // 🔄 步骤2.2:异步发送到会话事件队列
    // 这里实现了协程间的完全解耦
    return sendCmdToQueue(ctx, queue, cmd, 1*time.Second)
}

// sendCmdToQueue 安全发送命令到事件队列
func sendCmdToQueue(ctx context.Context, queue *EventQueue, value Cmd2Value, timeout time.Duration) error {
    select {
    case queue.queue <- &Event{Data: value}: // 成功发送到队列
        return nil
    case <-time.After(timeout): // 发送超时保护
        return errs.New("send cmd to queue timeout")
    case <-ctx.Done(): // 上下文取消
        return ctx.Err()
    }
}

步骤3:会话协程事件接收处理

// ConsumeConversationEventLoop 会话协程主循环
// 路径:openim-sdk-core/internal/conversation_msg/conversation_msg.go:1541-1576
func (c *Conversation) ConsumeConversationEventLoop(ctx context.Context) {
    // 🔄 步骤3.1:会话协程持续监听事件队列
    c.conversationEventQueue.ConsumeLoop(ctx,
        // 事件处理函数
        func(ctx context.Context, event *common.Event) {
            // 🔄 步骤3.2:解析事件数据
            cmd, ok := event.Data.(common.Cmd2Value)
            if !ok {
                log.ZWarn(ctx, "invalid event data in conversationEventQueue", nil)
                return
            }

            log.ZInfo(cmd.Ctx, "recv cmd", "caller", cmd.Caller, "cmd", cmd.Cmd)

            // 🔄 步骤3.3:调用Work方法处理具体事件
            c.Work(cmd)

            log.ZInfo(cmd.Ctx, "done cmd", "caller", cmd.Caller, "cmd", cmd.Cmd)
        },
        // 错误处理函数
        func(msg string, fields ...any) {
            log.ZError(ctx, msg, nil, fields...)
        })
}

步骤4:Work方法事件路由

// Work 处理会话事件(会话协程的核心调度方法)
// 路径:openim-sdk-core/internal/conversation_msg/notification.go:63-97
func (c *Conversation) Work(c2v common.Cmd2Value) {
    // 🔄 步骤4.1:根据命令类型进行路由分发
    switch c2v.Cmd {
    case constant.CmdNewMsgCome:
        // 🔄 步骤4.2:新消息事件 - 最终执行目标
        c.doMsgNew(c2v)  // 这就是triggerConversation最终执行的方法!
    default:
        log.ZWarn(context.Background(), "unknown cmd", nil, "cmd", c2v.Cmd)
    }
}

步骤5:doMsgNew最终执行方法

triggerConversation最终执行的就是doMsgNew方法!

// doMsgNew 处理新消息到达事件(triggerConversation的最终执行目标)
// 路径:openim-sdk-core/internal/conversation_msg/conversation_msg.go:220-694

// doMsgNew 处理新消息到达事件
// 这是OpenIM消息处理的核心方法,负责处理从服务器接收到的新消息
//
// ========== 为什么需要这么复杂的处理逻辑? ==========
// 1. 多端同步:消息可能来自不同设备(手机、电脑、网页),需要保证数据一致性
// 2. 离线在线:用户可能在离线状态错过消息,上线后需要同步历史消息
// 3. 消息去重:网络重传、多设备登录等可能导致重复消息,需要识别并处理
// 4. 状态管理:消息状态(发送中、成功、失败)需要正确维护和更新
// 5. 会话维护:消息到达需要更新对应会话的最新消息、未读数等信息
// 6. UI通知:不同场景需要不同的UI通知方式(在线、离线、前台、后台)
//
// ========== 主要处理流程 ==========
// 1. 消息分类:区分自己发送的消息和他人发送的消息
// 2. 重复检测:检查消息是否已存在,避免重复插入
// 3. 状态更新:更新消息状态(如补充服务器返回的seq)
// 4. 会话同步:创建或更新相关会话信息
// 5. 数据持久化:批量插入消息和会话到本地数据库
// 6. 事件通知:触发UI更新和用户通知
func (c *Conversation) doMsgNew(c2v common.Cmd2Value) {
	// ========== 初始化变量和数据结构 ==========
	allMsg := c2v.Value.(sdk_struct.CmdNewMsgComeToConversation).Msgs // 获取所有新消息,按会话ID分组
	ctx := c2v.Ctx                                                    // 获取上下文
	var isTriggerUnReadCount bool                                     // 是否触发未读计数变更事件

	// ========== 为什么要用映射结构? ==========
	// 1. 性能优化:按会话分组,减少数据库操作次数,提高批量插入效率
	// 2. 事务安全:同一会话的消息可以在同一事务中处理,保证数据一致性
	// 3. 内存优化:预分配容量,减少动态扩容的性能开销
	insertMsg := make(map[string][]*model_struct.LocalChatLog, 10) // 需要插入的新消息
	updateMsg := make(map[string][]*model_struct.LocalChatLog, 10) // 需要更新的消息(补充seq等信息)
	var exceptionMsg []*model_struct.LocalChatLog                  // 异常消息列表(重复消息等)
	var newMessages sdk_struct.NewMsgList                          // 需要通知UI的新消息列表

	// ========== 为什么需要这些标志位? ==========
	// 消息选项标志位决定了消息的处理方式,这是为了支持不同的业务场景:
	// - isHistory: 区分实时消息和历史消息,历史消息不触发通知
	// - isUnreadCount: 控制是否计入未读数,系统消息可能不计入未读
	// - isConversationUpdate: 控制是否更新会话信息,避免过期消息更新会话
	// - isNotPrivate: 区分普通消息和私聊消息,影响UI显示
	// - isSenderConversationUpdate: 发送者是否需要更新会话,避免自己的消息重复更新
	var isUnreadCount, isConversationUpdate, isHistory, isNotPrivate, isSenderConversationUpdate bool

	// ========== 为什么需要这么多会话集合? ==========
	// 1. conversationSet: 临时收集所有涉及的会话,用于后续比较
	// 2. conversationChangedSet: 已存在但需要更新的会话(如最新消息、未读数)
	// 3. newConversationSet: 全新的会话,需要首次创建
	// 4. phConversationChangedSet: 隐藏会话中的变更会话,保持用户的隐藏设置
	// 5. phNewConversationSet: 隐藏会话中的新会话,继承隐藏状态
	// 这样分类是为了:
	// - 减少数据库操作:批量插入和批量更新分别处理
	// - 保持用户设置:隐藏会话的状态需要特殊处理
	// - 触发正确事件:新会话和变更会话需要不同的UI通知
	conversationChangedSet := make(map[string]*model_struct.LocalConversation)   // 已变更的会话
	newConversationSet := make(map[string]*model_struct.LocalConversation)       // 新创建的会话
	conversationSet := make(map[string]*model_struct.LocalConversation)          // 所有涉及的会话
	phConversationChangedSet := make(map[string]*model_struct.LocalConversation) // 隐藏会话中的变更会话
	phNewConversationSet := make(map[string]*model_struct.LocalConversation)     // 隐藏会话中的新会话

	log.ZDebug(ctx, "message come here conversation ch", "conversation length", len(allMsg))
	b := time.Now() // 记录处理开始时间,用于性能统计和问题排查

	// ========== 为什么需要区分在线和离线消息? ==========
	// 1. 通知策略不同:在线消息立即通知,离线消息可能需要推送通知
	// 2. UI展示不同:在线消息可能有特殊的动画效果
	// 3. 统计需求:区分实时消息和同步消息,用于数据分析
	onlineMap := make(map[onlineMsgKey]struct{})

	// ========== 按会话处理消息主循环 ==========
	// 为什么按会话分组处理?
	// 1. 数据库优化:同一会话的消息可以批量插入,减少IO操作
	// 2. 事务完整性:同一会话的消息在同一事务中处理,保证数据一致性
	// 3. 会话状态:每个会话的最新消息、未读数等需要统一计算
	// 4. 内存管理:按会话处理可以及时释放内存,避免大批量消息占用过多内存
	for conversationID, msgs := range allMsg {
		log.ZDebug(ctx, "parse message in one conversation", "conversationID",
			conversationID, "message length", len(msgs.Msgs))

		// ========== 为什么要分类存储消息? ==========
		// 1. insertMessage: 异常消息(重复、删除等),需要特殊处理
		// 2. selfInsertMessage: 自己发送的消息,需要填充自己的头像昵称
		// 3. othersInsertMessage: 他人发送的消息,需要获取发送者信息
		// 4. updateMessage: 已存在的消息,只需要更新部分字段(如seq)
		// 这样分类的原因:
		// - 性能优化:不同类型的消息需要不同的处理逻辑
		// - 批量操作:相同类型的消息可以批量处理
		// - 用户体验:自己和他人的消息显示方式不同
		var insertMessage, selfInsertMessage, othersInsertMessage []*model_struct.LocalChatLog
		var updateMessage []*model_struct.LocalChatLog // 需要更新的消息(通常是补充seq信息)

		// ========== 处理会话中的每条消息 ==========
		for _, v := range msgs.Msgs {
			log.ZDebug(ctx, "parse message ", "conversationID", conversationID, "msg", v)

			// ========== 解析消息选项标志位 ==========
			// 为什么需要这些标志位?每个标志位解决什么问题?

			// isHistory: 区分实时消息和历史消息
			// 为什么需要?用户上线后同步历史消息时,不应该触发新消息通知
			// 场景:用户离线8小时后上线,同步100条历史消息,不应该弹出100个通知
			isHistory = utils.GetSwitchFromOptions(v.Options, constant.IsHistory)

			// isUnreadCount: 控制是否计入未读数
			// 为什么需要?某些系统消息(如入群通知)可能不希望影响未读数
			// 场景:管理员批量拉人入群,不应该让每个人都有大量未读数
			isUnreadCount = utils.GetSwitchFromOptions(v.Options, constant.IsUnreadCount)

			// isConversationUpdate: 控制是否更新会话信息
			// 为什么需要?避免旧消息覆盖会话的最新信息
			// 场景:同步历史消息时,不应该让旧消息成为会话的"最新消息"
			isConversationUpdate = utils.GetSwitchFromOptions(v.Options, constant.IsConversationUpdate)

			// isNotPrivate: 区分普通消息和私聊消息
			// 为什么需要?私聊模式下的消息有特殊的显示和处理逻辑
			// 场景:阅后即焚、私密会话等功能需要特殊标记
			isNotPrivate = utils.GetSwitchFromOptions(v.Options, constant.IsNotPrivate)

			// isSenderConversationUpdate: 发送者是否更新会话
			// 为什么需要?发送者和接收者的会话更新逻辑可能不同
			// 场景:发送者发送消息后不应该重复更新自己的会话信息
			isSenderConversationUpdate = utils.GetSwitchFromOptions(v.Options, constant.IsSenderConversationUpdate)

			// ========== 消息结构转换和数据准备 ==========
			msg := &sdk_struct.MsgStruct{}
			copier.Copy(msg, v)             // 复制基本字段
			msg.Content = string(v.Content) // 转换消息内容为字符串

			// 解析附加信息(如私聊标识、群组信息等)
			var attachedInfo sdk_struct.AttachedInfoElem
			_ = utils.JsonStringToStruct(v.AttachedInfo, &attachedInfo)
			msg.AttachedInfoElem = &attachedInfo

			// ========== 处理已删除消息 ==========
			// 当消息已被云端标记删除时,直接插入本地,不更新会话和消息状态
			if msg.Status == constant.MsgStatusHasDeleted {
				dbMessage := MsgStructToLocalChatLog(msg)
				c.handleExceptionMessages(ctx, nil, dbMessage) // 处理异常消息
				exceptionMsg = append(exceptionMsg, dbMessage)
				insertMessage = append(insertMessage, dbMessage)
				continue
			}

			// 设置消息状态为发送成功(从服务器接收的消息都认为是成功的)
			msg.Status = constant.MsgStatusSendSuccess

			// ========== 根据消息类型解析消息内容 ==========
			// 不同类型的消息需要不同的解析方式(文本、图片、文件等)
			err := msgHandleByContentType(msg)
			if err != nil {
				log.ZError(ctx, "Parsing data error:", err, "type: ", msg.ContentType, "msg", msg)
				continue
			}

			// ========== 设置私聊标识 ==========
			if !isNotPrivate {
				msg.AttachedInfoElem.IsPrivateChat = true
			}

			// ========== 验证会话ID ==========
			if conversationID == "" {
				log.ZError(ctx, "conversationID is empty", errors.New("conversationID is empty"), "msg", msg)
				continue
			}

			// ========== 标记在线消息 ==========
			if !isHistory {
				// 记录在线消息,用于后续区分在线和离线消息的通知方式
				onlineMap[onlineMsgKey{ClientMsgID: v.ClientMsgID, ServerMsgID: v.ServerMsgID}] = struct{}{}
				newMessages = append(newMessages, msg)
			}
			log.ZDebug(ctx, "decode message", "msg", msg)
			// ========== 根据发送者分类处理消息 ==========
			if v.SendID == c.loginUserID {
				// ========== 处理自己发送的消息 ==========
				// 为什么自己发送的消息也会收到?
				// 1. 多设备同步:在手机发送的消息需要同步到电脑和网页
				// 2. 状态确认:客户端发送消息后,服务器返回确认信息(seq、时间戳等)
				// 3. 历史同步:重装应用或切换设备时,需要同步自己的历史消息

				// 检查消息是否已存在于本地数据库
				existingMsg, err := c.db.GetMessage(ctx, conversationID, msg.ClientMsgID)
				if err == nil {
					// ========== 情况1:消息已存在,通常是状态更新 ==========
					log.ZInfo(ctx, "have message", "msg", msg)
					if existingMsg.Seq == 0 {
						// 为什么本地消息的seq会是0?
						// 发送消息的流程:客户端先本地插入消息(seq=0)-> 发送到服务器 -> 服务器返回seq
						// 这里收到的就是服务器返回的确认信息,需要更新本地消息的seq
						if !isConversationUpdate {
							// 为什么要标记为已过滤?
							// 某些场景下(如历史同步),不希望触发会话更新,但仍需要更新消息状态
							msg.Status = constant.MsgStatusFiltered
						}
						updateMessage = append(updateMessage, MsgStructToLocalChatLog(msg))
					} else {
						// 为什么消息已有seq还会再次收到?
						// 1. 网络重传:网络不稳定导致重复接收
						// 2. 多端登录:多个设备同时在线,重复处理
						// 3. 服务器重复推送:服务器异常导致重复发送
						// 这种情况属于异常,需要特殊处理避免数据冲突
						dbMessage := MsgStructToLocalChatLog(msg)
						c.handleExceptionMessages(ctx, existingMsg, dbMessage)
						insertMessage = append(insertMessage, dbMessage)
						exceptionMsg = append(exceptionMsg, dbMessage)
					}
				} else {
					// ========== 情况2:消息不存在于本地,这是同步消息 ==========
					// 为什么自己发送的消息本地会不存在?
					// 1. 其他设备发送:在其他设备(手机、网页)发送的消息
					// 2. API发送:通过服务器API发送的消息
					// 3. 历史同步:重装应用后同步的历史消息
					log.ZInfo(ctx, "sync message", "msg", msg)

					// 创建会话信息
					lc := model_struct.LocalConversation{
						ConversationType:  v.SessionType,
						LatestMsg:         utils.StructToJsonString(msg),
						LatestMsgSendTime: msg.SendTime,
						ConversationID:    conversationID,
					}

					// 根据会话类型设置相应字段
					switch v.SessionType {
					case constant.SingleChatType:
						lc.UserID = v.RecvID // 单聊时设置对方用户ID
					case constant.WriteGroupChatType, constant.ReadGroupChatType:
						lc.GroupID = v.GroupID // 群聊时设置群组ID
					}

					// 根据标志位决定是否更新会话和添加到新消息列表
					if isConversationUpdate {
						if isSenderConversationUpdate {
							log.ZDebug(ctx, "updateConversation msg", "message", v, "conversation", lc)
							c.updateConversation(&lc, conversationSet)
						}
						newMessages = append(newMessages, msg)
					}

					// 历史消息直接插入,不触发通知
					if isHistory {
						selfInsertMessage = append(selfInsertMessage, MsgStructToLocalChatLog(msg))
					}
				}
			} else {
				// ========== 处理他人发送的消息 ==========
				// 他人发送的消息处理逻辑相对简单,主要关注:
				// 1. 消息去重:避免重复插入相同消息
				// 2. 会话创建:为新的聊天对象创建会话
				// 3. 未读计数:正确统计未读消息数量
				// 4. 状态更新:更新会话的最新消息信息

				if existingMsg, err := c.db.GetMessage(ctx, conversationID, msg.ClientMsgID); err != nil {
					// ========== 新消息处理:创建会话信息 ==========
					// 为什么要创建会话信息?
					// 1. 会话列表显示:用户需要在会话列表中看到这个聊天
					// 2. 最新消息:会话列表需要显示最新的一条消息
					// 3. 时间排序:会话列表按最后活跃时间排序
					// 4. 未读统计:统计该会话的未读消息数量
					lc := model_struct.LocalConversation{
						ConversationType:  v.SessionType,
						LatestMsg:         utils.StructToJsonString(msg),
						LatestMsgSendTime: msg.SendTime,
						ConversationID:    conversationID,
					}

					// ========== 根据会话类型设置会话显示信息 ==========
					// 为什么需要按类型设置不同信息?
					// 1. 单聊:显示对方的头像和昵称
					// 2. 群聊:显示群组的头像和群名
					// 3. 通知:显示系统或机器人信息
					switch v.SessionType {
					case constant.SingleChatType:
						// 单聊:设置发送者信息作为会话显示信息
						// 为什么用发送者信息?单聊中对方就是发送者
						lc.UserID = v.SendID
						lc.ShowName = msg.SenderNickname
						lc.FaceURL = msg.SenderFaceURL
					case constant.WriteGroupChatType, constant.ReadGroupChatType:
						// 群聊:设置群组ID,稍后会批量获取群组信息
						lc.GroupID = v.GroupID
					case constant.NotificationChatType:
						// 通知类型:通常是系统消息或机器人消息
						lc.UserID = v.SendID
					}

					// ========== 处理未读计数 ==========
					// 为什么未读计数这么重要且复杂?
					// 1. 用户体验:未读数是用户最关心的信息之一
					// 2. 性能影响:错误的未读数会导致不必要的UI更新
					// 3. 多端同步:多个设备的未读数需要保持一致
					// 4. 业务逻辑:某些消息不应计入未读数(如系统通知)
					if isUnreadCount {
						// 为什么要检查是否为新消息?
						// 1. 避免重复计数:同一条消息不应该被重复计入未读数
						// 2. 历史消息:同步历史消息时不应该增加未读数
						// 3. 序列号判断:通过比较序列号确定消息的新旧程度
						// maxSeqRecorder记录的是用户已知的最大序列号
						if c.maxSeqRecorder.IsNewMsg(conversationID, msg.Seq) {
							isTriggerUnReadCount = true              // 标记需要触发未读计数变更事件
							lc.UnreadCount = 1                       // 设置未读数为1(单条新消息)
							c.maxSeqRecorder.Incr(conversationID, 1) // 更新序列号记录器

							// 为什么用Incr而不是直接设置?
							// 1. 并发安全:多条消息可能同时处理
							// 2. 增量更新:只增加新消息的数量
							// 3. 状态一致:确保记录器状态与实际处理保持同步
						}
					}

					// ========== 更新会话信息 ==========
					if isConversationUpdate {
						c.updateConversation(&lc, conversationSet)
						newMessages = append(newMessages, msg)
					}

					// ========== 处理历史消息 ==========
					if isHistory {
						othersInsertMessage = append(othersInsertMessage, MsgStructToLocalChatLog(msg))
					}

				} else {
					// 消息已存在,这是重复消息,标记为异常
					dbMessage := MsgStructToLocalChatLog(msg)
					c.handleExceptionMessages(ctx, existingMsg, dbMessage)
					insertMessage = append(insertMessage, dbMessage)
					exceptionMsg = append(exceptionMsg, dbMessage)
				}
			}
		}

		// ========== 整理当前会话的消息 ==========
		// 合并所有需要插入的消息,并处理头像昵称信息
		insertMsg[conversationID] = append(insertMessage, c.faceURLAndNicknameHandle(ctx, selfInsertMessage, othersInsertMessage, conversationID)...)

		// 收集需要更新的消息
		if len(updateMessage) > 0 {
			updateMsg[conversationID] = updateMessage
		}
	}

	// ========== 数据库操作同步锁 ==========
	// 为什么需要这个锁?
	// 1. 数据一致性:防止并发处理导致的数据冲突
	// 2. 事务完整性:确保消息和会话的更新是原子操作
	// 3. 状态同步:避免会话状态计算错误(如未读数、最新消息)
	// 4. 去重保证:防止重复消息被并发插入
	//
	// 为什么是全局锁而不是细粒度锁?
	// 1. 简单可靠:全局锁虽然性能较差,但能保证绝对的数据一致性
	// 2. 跨会话操作:某些操作可能涉及多个会话,细粒度锁容易死锁
	// 3. 复杂性权衡:细粒度锁需要复杂的锁排序和死锁检测机制
	//
	// TODO: 锁粒度优化方案
	// 1. 会话级别锁:按conversationID进行锁分片
	// 2. 读写分离:读操作使用读锁,写操作使用写锁
	// 3. 无锁设计:使用CAS操作和版本号机制
	c.conversationSyncMutex.Lock()
	defer c.conversationSyncMutex.Unlock()

	// ========== 获取本地所有会话并进行差异比较 ==========
	list, err := c.db.GetAllConversationListDB(ctx)
	if err != nil {
		log.ZError(ctx, "GetAllConversationListDB", err)
	}

	// 将会话列表转换为映射,便于查找和比较
	m := make(map[string]*model_struct.LocalConversation)
	listToMap(list, m)
	log.ZDebug(ctx, "listToMap: ", "local conversation", list, "generated c map",
		string(stringutil.StructToJsonBytes(conversationSet)))

	// 比较本地会话和新生成的会话,区分出变更和新增的会话
	c.diff(ctx, m, conversationSet, conversationChangedSet, newConversationSet)
	log.ZInfo(ctx, "trigger map is :", "newConversations", string(stringutil.StructToJsonBytes(newConversationSet)),
		"changedConversations", string(stringutil.StructToJsonBytes(conversationChangedSet)))

	// ========== 批量更新消息(主要是补充seq信息) ==========
	if err := c.batchUpdateMessageList(ctx, updateMsg); err != nil {
		log.ZError(ctx, "sync seq normal message err  :", err)
	}

	// ========== 批量插入新消息 ==========
	_ = c.batchInsertMessageList(ctx, insertMsg)

	// ========== 处理隐藏会话列表 ==========
	// 为什么需要隐藏会话机制?
	// 1. 用户体验:用户可以隐藏不常用的会话,保持界面清洁
	// 2. 设置保持:隐藏的会话仍需保留用户的个性化设置(置顶、免打扰等)
	// 3. 消息接收:隐藏不等于删除,仍需要接收和处理消息
	// 4. 恢复机制:用户发送消息或收到新消息时,会话会自动显示
	//
	// 隐藏会话的生命周期:
	// 正常会话 -> 用户隐藏 -> 隐藏会话 -> 有新消息 -> 显示会话
	hList, _ := c.db.GetHiddenConversationList(ctx)
	for _, v := range hList {
		if nc, ok := newConversationSet[v.ConversationID]; ok {
			// ========== 隐藏会话复活处理 ==========
			// 为什么要"复活"隐藏会话?
			// 当隐藏的会话有新消息时,应该重新显示给用户
			// 但需要保持用户之前的所有设置

			// 将新会话移到隐藏会话变更集合中,并继承隐藏会话的设置
			phConversationChangedSet[v.ConversationID] = nc

			// ========== 继承用户设置 ==========
			// 为什么要继承这些设置?保持用户的个性化配置
			nc.RecvMsgOpt = v.RecvMsgOpt       // 继承接收消息选项(免打扰等)
			nc.GroupAtType = v.GroupAtType     // 继承群组@类型(是否被@过)
			nc.IsPinned = v.IsPinned           // 继承置顶状态
			nc.IsPrivateChat = v.IsPrivateChat // 继承私聊状态(阅后即焚等)
			if nc.IsPrivateChat {
				nc.BurnDuration = v.BurnDuration // 继承阅后即焚时长
			}
			if v.UnreadCount != 0 {
				// 为什么要继承未读数?
				// 隐藏期间可能积累了未读消息,这些应该保留
				nc.UnreadCount = v.UnreadCount // 继承未读数
			}
			nc.IsNotInGroup = v.IsNotInGroup       // 继承是否在群组状态
			nc.AttachedInfo = v.AttachedInfo       // 继承附加信息
			nc.Ex = v.Ex                           // 继承扩展字段
			nc.IsMsgDestruct = v.IsMsgDestruct     // 继承消息销毁设置
			nc.MsgDestructTime = v.MsgDestructTime // 继承消息销毁时间
		}
	}

	// ========== 分离真正的新会话 ==========
	// 为什么要分离?
	// 1. 数据库操作优化:新会话和恢复会话使用不同的SQL操作
	// 2. 事件通知:新会话和恢复会话需要触发不同的UI事件
	// 3. 状态管理:区分首次创建和重新激活,便于统计和分析
	for k, v := range newConversationSet {
		if _, ok := phConversationChangedSet[v.ConversationID]; !ok {
			phNewConversationSet[k] = v
		}
	}

	// ========== 批量更新会话信息 ==========
	if err := c.db.BatchUpdateConversationList(ctx, append(mapConversationToList(conversationChangedSet), mapConversationToList(phConversationChangedSet)...)); err != nil {
		log.ZError(ctx, "insert changed conversation err :", err)
	}

	// ========== 批量插入新会话 ==========
	if err := c.db.BatchInsertConversationList(ctx, mapConversationToList(phNewConversationSet)); err != nil {
		log.ZError(ctx, "insert new conversation err:", err)
	}
	log.ZDebug(ctx, "before trigger msg", "cost time", time.Since(b).Seconds(), "len", len(allMsg))

	// ========== 触发新消息事件 ==========
	c.newMessage(ctx, newMessages, conversationChangedSet, newConversationSet, onlineMap)

	// ========== 触发会话变更事件 ==========
	if len(newConversationSet) > 0 {
		// 触发新会话事件
		c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.NewConDirect, Args: utils.StructToJsonString(mapConversationToList(newConversationSet))}})
	}
	if len(conversationChangedSet) > 0 {
		// 触发会话变更事件
		c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChangeDirect, Args: utils.StructToJsonString(mapConversationToList(conversationChangedSet))}})
	}

	// ========== 触发未读计数变更事件 ==========
	if isTriggerUnReadCount {
		c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged, Args: ""}})
	}

	// ========== 处理输入状态消息 ==========
	for _, msgs := range allMsg {
		for _, msg := range msgs.Msgs {
			if msg.ContentType == constant.Typing {
				c.typing.onNewMsg(ctx, msg) // 处理正在输入状态
			}
		}
	}

	// ========== 记录异常消息 ==========
	for _, v := range exceptionMsg {
		log.ZWarn(ctx, "exceptionMsg show: ", nil, "msg", *v)
	}

	log.ZDebug(ctx, "insert msg", "duration", fmt.Sprintf("%dms", time.Since(b)), "len", len(allMsg))
}

## 第五步:doMsgNew方法完整逻辑解析

### 5.1 doMsgNew方法整体架构

`doMsgNew`是OpenIM消息处理的核心方法,负责处理从服务器接收到的新消息。这个方法设计复杂但逻辑清晰,主要解决以下关键问题:

1. **多端同步**:确保消息在不同设备间正确同步
2. **消息去重**:防止重复消息被多次处理
3. **会话管理**:创建和更新相关会话信息
4. **状态维护**:正确维护消息和会话的各种状态
5. **事件通知**:触发相应的UI更新事件

### 5.2 消息处理的核心数据结构

```go
// 消息分类存储映射
insertMsg := make(map[string][]*model_struct.LocalChatLog, 10) // 按会话ID分组的新消息
updateMsg := make(map[string][]*model_struct.LocalChatLog, 10) // 需要更新的消息(补充seq等)
var exceptionMsg []*model_struct.LocalChatLog                  // 异常消息(重复、删除等)
var newMessages sdk_struct.NewMsgList                          // 需要通知UI的新消息

// 会话状态集合
conversationChangedSet := make(map[string]*model_struct.LocalConversation)   // 已变更的会话
newConversationSet := make(map[string]*model_struct.LocalConversation)       // 新创建的会话
conversationSet := make(map[string]*model_struct.LocalConversation)          // 所有涉及的会话
phConversationChangedSet := make(map[string]*model_struct.LocalConversation) // 隐藏会话中的变更
phNewConversationSet := make(map[string]*model_struct.LocalConversation)     // 隐藏会话中的新增

// 在线消息标识
onlineMap := make(map[onlineMsgKey]struct{}) // 标识哪些是在线消息

5.3 消息选项标志位解析

每条消息都携带选项标志位,控制消息的处理方式:

// 消息选项解析
isHistory := utils.GetSwitchFromOptions(v.Options, constant.IsHistory)
isUnreadCount := utils.GetSwitchFromOptions(v.Options, constant.IsUnreadCount)
isConversationUpdate := utils.GetSwitchFromOptions(v.Options, constant.IsConversationUpdate)
isNotPrivate := utils.GetSwitchFromOptions(v.Options, constant.IsNotPrivate)
isSenderConversationUpdate := utils.GetSwitchFromOptions(v.Options, constant.IsSenderConversationUpdate)

标志位说明:

  • isHistory: 区分实时消息和历史消息,历史消息不触发通知
  • isUnreadCount: 控制是否计入未读数,某些系统消息不计入
  • isConversationUpdate: 控制是否更新会话信息,避免旧消息更新会话
  • isNotPrivate: 区分普通消息和私聊消息
  • isSenderConversationUpdate: 发送者是否需要更新会话

5.4 消息处理的双分支逻辑

5.4.1 处理自己发送的消息
if v.SendID == c.loginUserID {
    // 自己发送的消息有两种情况:
    // 1. 多设备同步:其他设备发送的消息同步到当前设备
    // 2. 状态确认:发送后服务器返回的确认信息(补充seq等)
    
    existingMsg, err := c.db.GetMessage(ctx, conversationID, msg.ClientMsgID)
    if err == nil {
        // 消息已存在,通常是状态更新
        if existingMsg.Seq == 0 {
            // 本地消息seq为0,这是服务器返回的确认信息
            updateMessage = append(updateMessage, MsgStructToLocalChatLog(msg))
        } else {
            // 消息已有seq,这是重复消息,标记为异常
            dbMessage := MsgStructToLocalChatLog(msg)
            c.handleExceptionMessages(ctx, existingMsg, dbMessage)
            exceptionMsg = append(exceptionMsg, dbMessage)
        }
    } else {
        // 消息不存在,这是来自其他设备的同步消息
        // 创建会话信息并根据标志位决定是否更新
    }
}
5.4.2 处理他人发送的消息
else {
    // 他人发送的消息处理相对简单
    if existingMsg, err := c.db.GetMessage(ctx, conversationID, msg.ClientMsgID); err != nil {
        // 新消息:创建会话信息
        lc := model_struct.LocalConversation{
            ConversationType:  v.SessionType,
            LatestMsg:         utils.StructToJsonString(msg),
            LatestMsgSendTime: msg.SendTime,
            ConversationID:    conversationID,
        }
        
        // 处理未读计数
        if isUnreadCount && c.maxSeqRecorder.IsNewMsg(conversationID, msg.Seq) {
            isTriggerUnReadCount = true
            lc.UnreadCount = 1
            c.maxSeqRecorder.Incr(conversationID, 1)
        }
        
        // 更新会话信息
        if isConversationUpdate {
            c.updateConversation(&lc, conversationSet)
            newMessages = append(newMessages, msg)
        }
    } else {
        // 重复消息,标记为异常
        dbMessage := MsgStructToLocalChatLog(msg)
        c.handleExceptionMessages(ctx, existingMsg, dbMessage)
        exceptionMsg = append(exceptionMsg, dbMessage)
    }
}

5.5 消息和会话属性变化详解

5.5.1 消息属性的初始化和更新

当新消息到达时,消息对象的关键属性处理:

// 消息基本属性设置
msg.Status = constant.MsgStatusSendSuccess // 从服务器接收的消息都标记为成功
msg.IsRead = false                         // 新消息初始为未读状态
msg.Seq = v.Seq                           // 服务器分配的序列号
msg.ServerMsgID = v.ServerMsgID           // 服务器生成的消息ID
msg.SendTime = v.SendTime                 // 实际发送时间
msg.CreateTime = v.CreateTime             // 消息创建时间

// 解析消息内容和附加信息
err := msgHandleByContentType(msg)        // 根据消息类型解析内容
var attachedInfo sdk_struct.AttachedInfoElem
_ = utils.JsonStringToStruct(v.AttachedInfo, &attachedInfo)
msg.AttachedInfoElem = &attachedInfo      // 设置附加信息
5.5.2 会话属性的创建和更新
// 会话信息创建
lc := model_struct.LocalConversation{
    ConversationID:       conversationID,           // 会话唯一标识
    ConversationType:     v.SessionType,            // 会话类型(单聊/群聊)
    LatestMsg:           utils.StructToJsonString(msg), // 最新消息JSON
    LatestMsgSendTime:   msg.SendTime,              // 最新消息时间
    UnreadCount:         0,                         // 初始未读数
    MaxSeq:             msg.Seq,                    // 最大序列号
    MinSeq:             msg.Seq,                    // 最小序列号(首条消息时)
}

// 根据会话类型设置特定属性
switch v.SessionType {
case constant.SingleChatType:
    lc.UserID = v.SendID              // 单聊:设置对方用户ID
    lc.ShowName = msg.SenderNickname  // 显示对方昵称
    lc.FaceURL = msg.SenderFaceURL    // 显示对方头像
case constant.ReadGroupChatType:
    lc.GroupID = v.GroupID            // 群聊:设置群组ID
}
5.5.3 MaxSeq和ReadSeq状态管理
// MaxSeq状态管理
if c.maxSeqRecorder.IsNewMsg(conversationID, msg.Seq) {
    // 这是一条新消息(序列号大于已知最大值)
    c.maxSeqRecorder.Incr(conversationID, 1)  // 更新内存中的最大序列号
    
    // 同时更新会话的MaxSeq
    if conversation != nil {
        conversation.MaxSeq = msg.Seq
    }
}

// ReadSeq的处理在后续的已读回执中进行
// 用户阅读消息后,会调用markConversationMessageAsRead方法更新ReadSeq
5.5.4 未读数量计算机制
// 未读数量的精确计算
if isUnreadCount && c.maxSeqRecorder.IsNewMsg(conversationID, msg.Seq) {
    // 只有满足以下条件的消息才计入未读数:
    // 1. 消息选项允许计入未读数(isUnreadCount = true)
    // 2. 消息序列号大于已知最大序列号(是新消息)
    // 3. 不是历史同步消息(isHistory = false)
    
    isTriggerUnReadCount = true              // 标记需要触发未读数变更事件
    lc.UnreadCount = 1                       // 新会话未读数为1
    
    // 如果是已存在会话,累加未读数
    if existingConversation != nil {
        existingConversation.UnreadCount += 1
    }
    
    // 更新内存中的序列号记录
    c.maxSeqRecorder.Incr(conversationID, 1)
}

5.6 数据库操作和事务性

5.6.1 同步锁保护
// 全局同步锁确保数据一致性
c.conversationSyncMutex.Lock()
defer c.conversationSyncMutex.Unlock()

// 锁的作用:
// 1. 防止并发处理导致的数据冲突
// 2. 确保消息和会话的更新是原子操作
// 3. 避免重复消息被并发插入
// 4. 保证状态计算的准确性(如未读数、最新消息)
5.6.2 批量数据库操作
// 批量更新消息(主要是补充seq信息)
if err := c.batchUpdateMessageList(ctx, updateMsg); err != nil {
    log.ZError(ctx, "sync seq normal message err:", err)
}

// 批量插入新消息
_ = c.batchInsertMessageList(ctx, insertMsg)

// 批量更新会话信息
conversationsToUpdate := append(
    mapConversationToList(conversationChangedSet),
    mapConversationToList(phConversationChangedSet)...,
)
if err := c.db.BatchUpdateConversationList(ctx, conversationsToUpdate); err != nil {
    log.ZError(ctx, "update conversations err:", err)
}

// 批量插入新会话
if err := c.db.BatchInsertConversationList(ctx, mapConversationToList(phNewConversationSet)); err != nil {
    log.ZError(ctx, "insert new conversations err:", err)
}

5.7 事件通知机制

5.7.1 消息事件通知
// 触发新消息事件
c.newMessage(ctx, newMessages, conversationChangedSet, newConversationSet, onlineMap)

// newMessage方法内部根据应用状态选择通知方式:
if c.GetBackground() {
    // 后台模式:检查接收设置后触发离线消息通知
    for _, w := range newMessagesList {
        if shouldNotify(w) {
            c.msgListener().OnRecvOfflineNewMessage(utils.StructToJsonString(w))
        }
    }
} else {
    // 前台模式:区分在线消息和普通消息
    for _, w := range newMessagesList {
        if isOnlineMsg(w) {
            c.msgListener().OnRecvOnlineOnlyMessage(utils.StructToJsonString(w))
        } else {
            c.msgListener().OnRecvNewMessage(utils.StructToJsonString(w))
        }
    }
}
5.7.2 会话事件通知
// 触发新会话事件
if len(newConversationSet) > 0 {
    c.doUpdateConversation(common.Cmd2Value{
        Value: common.UpdateConNode{
            Action: constant.NewConDirect,
            Args:   utils.StructToJsonString(mapConversationToList(newConversationSet)),
        },
    })
}

// 触发会话变更事件
if len(conversationChangedSet) > 0 {
    c.doUpdateConversation(common.Cmd2Value{
        Value: common.UpdateConNode{
            Action: constant.ConChangeDirect,
            Args:   utils.StructToJsonString(mapConversationToList(conversationChangedSet)),
        },
    })
}

// 触发未读计数变更事件
if isTriggerUnReadCount {
    c.doUpdateConversation(common.Cmd2Value{
        Value: common.UpdateConNode{
            Action: constant.TotalUnreadMessageChanged,
            Args:   "",
        },
    })
}

5.8 特殊功能处理

5.8.1 隐藏会话复活机制
// 处理隐藏会话列表
hList, _ := c.db.GetHiddenConversationList(ctx)
for _, v := range hList {
    if nc, ok := newConversationSet[v.ConversationID]; ok {
        // 隐藏会话有新消息时,需要重新显示
        phConversationChangedSet[v.ConversationID] = nc
        
        // 继承用户的所有个性化设置
        nc.RecvMsgOpt = v.RecvMsgOpt           // 接收消息选项
        nc.IsPinned = v.IsPinned               // 置顶状态
        nc.IsPrivateChat = v.IsPrivateChat     // 私聊设置
        nc.BurnDuration = v.BurnDuration       // 阅后即焚时长
        nc.UnreadCount = v.UnreadCount         // 继承未读数
        // ... 其他设置
    }
}
5.8.2 输入状态处理
// 处理正在输入状态消息
for _, msgs := range allMsg {
    for _, msg := range msgs.Msgs {
        if msg.ContentType == constant.Typing {
            c.typing.onNewMsg(ctx, msg) // 触发输入状态更新
        }
    }
}

5.9 性能优化和错误处理

5.9.1 批量操作优化
// 消息分类处理,提高批量操作效率
selfInsertMessage := []*model_struct.LocalChatLog{}     // 自己发送的消息
othersInsertMessage := []*model_struct.LocalChatLog{}   // 他人发送的消息

// 批量填充头像昵称信息
mergedMessages := c.faceURLAndNicknameHandle(ctx, selfInsertMessage, othersInsertMessage, conversationID)
insertMsg[conversationID] = mergedMessages
5.9.2 异常消息处理详解
// handleExceptionMessages 处理异常消息的插入到本地聊天记录
// 根据message_check.go源码实现的完整逻辑
//
// 该方法处理从服务器拉取到的各种异常消息情况,确保本地数据的完整性和一致性
// 识别并标记属于以下类别的消息:
//
// ========== 异常消息类型详解 ==========
//
// 【类型1】云端已删除的消息 (message.Status == constant.MsgStatusHasDeleted)
//   - 含义:消息在云端被删除(通过撤回、管理员删除等操作)
//   - 特征:从服务器拉取时状态已标记为已删除
//   - 处理:作为占位消息保存,维护序列号连续性
//
//     子情况1.1: message.ClientMsgID == ""
//       - 含义:这是系统推送的消息(如:系统通知、入群通知、踢人通知等)
//       - 原因:系统消息通常由服务器直接生成,没有客户端消息ID
//       - 特点:这类消息被删除后ClientMsgID会被清空
//       - 处理:生成新的ClientMsgID,标记为序列号间隙占位符
//
//     子情况1.2: message.ClientMsgID != ""
//       - 含义:普通用户消息被删除,但ClientMsgID保留
//       - 原因:用户发送的消息被撤回或管理员删除
//       - 处理:保留ClientMsgID,标记为已删除消息
//
// 【类型2】序列号间隙填充消息
//   - 产生原因:服务器宕机、网络中断、长时间离线等导致序列号不连续
//   - 特征:拉取消息时发现序列号中间有空缺
//   - 处理:创建占位消息填补间隙,确保时间线完整
//
// 【类型3】ClientMsgID重复但序列号不同
//   - 产生原因:客户端重发、服务端重复消费、网络重试等
//   - 处理:标记为客户端重复消息
//
// 【类型4】ClientMsgID和序列号都重复
//   - 产生原因:并发消息拉取、重复请求等
//   - 处理:标记为序列号重复消息
//
// 参数:
//   - ctx: 上下文
//   - existingMessage: 本地已存在的消息(nil表示本地不存在)
//   - message: 从服务器拉取的待处理消息
func (c *Conversation) handleExceptionMessages(ctx context.Context, existingMessage, message *model_struct.LocalChatLog) {
    var prefix string

    // ========== 根据异常类型确定前缀标识 ==========
    if existingMessage == nil {
        // ========== 情况1:本地不存在该消息 ==========
        if message.Status == constant.MsgStatusHasDeleted {
            // 云端消息已被删除(撤回、管理员删除等)
            if message.ClientMsgID == "" {
                // 子情况1.1:系统推送消息被删除后ClientMsgID被清空
                // 例如:入群通知、踢人通知、系统公告等被管理员删除
                // 系统消息由服务器生成,删除后ClientMsgID会被清空
                // 需要重新生成ClientMsgID防止数据库主键冲突
                message.ClientMsgID = utils.GetMsgID(c.loginUserID)
                prefix = "[SYS_DEL_+" + utils.Int64ToString(message.Seq) + "]" // 系统消息删除占位符
            } else {
                // 子情况1.2:普通用户消息被删除,ClientMsgID保留
                // 例如:用户撤回消息、管理员删除用户消息等
                prefix = "[USER_DEL]" // 用户消息删除标记
            }
        } else {
            // 正常消息,不应该进入异常处理流程
            prefix = "[UNKNOWN]"
            log.ZWarn(ctx, "Normal message should not be handled as exception", nil, "message", message)
        }
    } else {
        // ========== 情况2:本地已存在相同ClientMsgID的消息 ==========
        if existingMessage.Seq == message.Seq {
            // ClientMsgID和序列号都重复 - 并发拉取导致
            // 产生原因:多线程同时拉取消息、网络重试、重复请求等
            prefix = "[CONCURRENT_DUP]" // 并发重复消息
        } else {
            // ClientMsgID重复但序列号不同 - 消息重发导致
            // 产生原因:客户端网络重试、服务端重复消费、消息重发等
            prefix = "[RESEND_DUP]" // 重发重复消息
        }
    }

    // ========== 生成随机字符串函数 ==========
    getRandomString := func(length int) string {
        const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))

        b := make([]byte, length)
        for i := range b {
            b[i] = charset[seededRand.Intn(len(charset))]
        }
        return string(b)
    }

    // ========== 统一处理异常消息 ==========
    // 生成8位随机后缀确保ClientMsgID唯一性,避免数据库主键冲突
    randomSuffix := "_" + getRandomString(8)

    // 统一标记为已删除状态,前端可根据前缀判断显示方式
    // - 系统删除消息:可显示为"系统消息已删除"
    // - 用户删除消息:可显示为"消息已撤回"
    // - 重复消息:可隐藏不显示或显示调试信息
    message.Status = constant.MsgStatusHasDeleted

    // 构造新的ClientMsgID:[异常类型前缀] + [原ClientMsgID] + [随机后缀]
    // 这样既保留了原始信息又确保了唯一性,便于问题排查和调试
    message.ClientMsgID = prefix + message.ClientMsgID + randomSuffix
}

// 使用示例和异常消息记录
func exampleUsage() {
    // 记录所有异常消息供调试分析
    for _, v := range exceptionMsg {
        log.ZWarn(ctx, "exceptionMsg show:", nil, "msg", *v)
    }
}

异常消息处理的核心设计原则:

  1. 数据完整性保证:即使是异常消息也要保存,确保消息序列号的连续性
  2. 唯一性保证:通过前缀+随机后缀避免ClientMsgID冲突
  3. 可追溯性:通过特殊前缀标识异常类型,便于问题排查
  4. 状态一致性:统一标记为已删除状态,前端可根据前缀决定显示方式

常见异常消息前缀含义:

  • [SYS_DEL_+seq]: 系统消息被删除的占位符
  • [USER_DEL]: 用户消息被删除/撤回
  • [CONCURRENT_DUP]: 并发拉取导致的重复消息
  • [RESEND_DUP]: 重发导致的重复消息
  • [UNKNOWN]: 未知类型的异常消息

5.10 doMsgNew完整流程图

graph TD
    A[doMsgNew开始] --> B[初始化数据结构]
    B --> C[遍历所有会话的消息]
    
    C --> D{消息循环}
    D --> E[解析消息选项标志位]
    E --> F[消息结构转换]
    F --> G[验证会话ID]
    
    G --> H{检查消息状态}
    H -->|已删除| I[处理已删除消息]
    H -->|正常| J[设置消息状态为成功]
    
    J --> K[解析消息内容]
    K --> L{检查发送者}
    
    L -->|自己发送| M[处理自己的消息]
    L -->|他人发送| N[处理他人的消息]
    
    M --> O{消息是否存在}
    O -->|存在| P{检查Seq}
    P -->|Seq=0| Q[更新消息状态]
    P -->|Seq>0| R[标记重复消息]
    O -->|不存在| S[同步消息处理]
    
    N --> T{消息是否存在}
    T -->|不存在| U[创建会话信息]
    T -->|存在| V[标记重复消息]
    
    U --> W{检查未读计数标志}
    W -->|允许| X[计算未读数]
    W -->|不允许| Y[跳过未读计数]
    
    X --> Z{是否为新消息}
    Z -->|是| AA[增加未读数]
    Z -->|否| Y
    
    AA --> BB[更新序列号记录器]
    BB --> CC[更新会话信息]
    
    Q --> DD[消息分类存储]
    R --> DD
    S --> DD
    V --> DD
    Y --> DD
    CC --> DD
    
    DD --> EE{是否处理完所有消息}
    EE -->|否| D
    EE -->|是| FF[获取数据库同步锁]
    
    FF --> GG[获取本地会话列表]
    GG --> HH[比较会话差异]
    HH --> II[处理隐藏会话]
    
    II --> JJ[批量更新消息]
    JJ --> KK[批量插入消息]
    KK --> LL[批量更新会话]
    LL --> MM[批量插入新会话]
    
    MM --> NN[触发新消息事件]
    NN --> OO[触发会话变更事件]
    OO --> PP[触发未读数变更事件]
    PP --> QQ[处理输入状态消息]
    QQ --> RR[记录异常消息]
    RR --> SS[doMsgNew结束]
    
    style A fill:#e1f5fe
    style SS fill:#e8f5e8
    style FF fill:#fff3e0
    style NN fill:#f3e5f5
    style X fill:#ffebee
    style Z fill:#ffebee

第六步:云端PullMessageBySeqList实现详解

6.1 服务端消息拉取处理概述

当客户端发起PullMessageBySeqList请求时,服务端需要:

  1. 验证用户权限和会话访问权限
  2. 按序列号范围查询消息数据
  3. 处理消息内容的过滤和权限控制
  4. 返回分页结果和边界标识

6.2 PullMessageBySeqs核心实现

// PullMessageBySeqs 服务端消息拉取处理
// 路径:open-im-server/internal/rpc/msg/sync_msg.go
func (m *msgServer) PullMessageBySeqs(ctx context.Context, req *sdkws.PullMessageBySeqsReq) (*sdkws.PullMessageBySeqsResp, error) {
    // 初始化响应结构,分别存储普通消息和通知消息
    resp := &sdkws.PullMessageBySeqsResp{}
    resp.Msgs = make(map[string]*sdkws.PullMsgs)             // 普通消息映射
    resp.NotificationMsgs = make(map[string]*sdkws.PullMsgs) // 通知消息映射

    // 遍历所有请求的序列号范围,支持批量处理多个会话
    for _, seq := range req.SeqRanges {
        if !msgprocessor.IsNotification(seq.ConversationID) {
            // 处理普通会话消息(单聊、群聊)
            conversation, err := m.ConversationLocalCache.GetConversation(ctx, req.UserID, seq.ConversationID)
            if err != nil {
                log.ZError(ctx, "GetConversation error", err, "conversationID", seq.ConversationID)
                continue
            }

            // 按序列号范围拉取消息,使用用户的MaxSeq进行权限控制
            minSeq, maxSeq, msgs, err := m.MsgDatabase.GetMsgBySeqsRange(ctx, req.UserID, seq.ConversationID,
                seq.Begin, seq.End, seq.Num, conversation.MaxSeq)
            if err != nil {
                log.ZWarn(ctx, "GetMsgBySeqsRange error", err)
                continue
            }

            // 判断是否已到达拉取边界
            var isEnd bool
            switch req.Order {
            case sdkws.PullOrder_PullOrderAsc:  // 升序拉取
                isEnd = maxSeq <= seq.End
            case sdkws.PullOrder_PullOrderDesc: // 降序拉取
                isEnd = seq.Begin <= minSeq
            }

            if len(msgs) > 0 {
                resp.Msgs[seq.ConversationID] = &sdkws.PullMsgs{Msgs: msgs, IsEnd: isEnd}
            }
        } else {
            // 处理通知类型消息
            var seqs []int64
            for i := seq.Begin; i <= seq.End; i++ {
                seqs = append(seqs, i)
            }

            minSeq, maxSeq, notificationMsgs, err := m.MsgDatabase.GetMsgBySeqs(ctx, req.UserID, seq.ConversationID, seqs)
            if err != nil {
                log.ZWarn(ctx, "GetMsgBySeqs error", err)
                continue
            }

            var isEnd bool
            switch req.Order {
            case sdkws.PullOrder_PullOrderAsc:
                isEnd = maxSeq <= seq.End
            case sdkws.PullOrder_PullOrderDesc:
                isEnd = seq.Begin <= minSeq
            }

            if len(notificationMsgs) > 0 {
                resp.NotificationMsgs[seq.ConversationID] = &sdkws.PullMsgs{Msgs: notificationMsgs, IsEnd: isEnd}
            }
        }
    }
    return resp, nil
}

6.3 数据库层getMsgBySeqs详细实现

// getMsgBySeqs 数据库层按序列号获取消息的核心实现
// 路径:open-im-server/pkg/common/storage/controller/msg.go:288-290
func (db *commonMsgDatabase) getMsgBySeqs(ctx context.Context, userID, conversationID string, seqs []int64) (totalMsgs []*sdkws.MsgData, err error) {
    // 1. 获取会话的序列号边界
    minSeq, err := db.seqConversation.GetMinSeq(ctx, conversationID)
    if err != nil {
        return nil, errs.WrapMsg(err, "GetMinSeq failed", "conversationID", conversationID)
    }
    
    maxSeq, err := db.seqConversation.GetMaxSeq(ctx, conversationID)
    if err != nil {
        return nil, errs.WrapMsg(err, "GetMaxSeq failed", "conversationID", conversationID)
    }

    // 2. 过滤有效的序列号(在边界范围内)
    var validSeqs []int64
    for _, seq := range seqs {
        if seq >= minSeq && seq <= maxSeq {
            validSeqs = append(validSeqs, seq)
        }
    }

    if len(validSeqs) == 0 {
        return nil, nil // 没有有效序列号
    }

    // 3. 从MongoDB获取消息数据
    msgs, err := db.GetMessageBySeqs(ctx, conversationID, userID, validSeqs)
    if err != nil {
        return nil, errs.WrapMsg(err, "GetMessageBySeqs failed")
    }

    // 4. 处理消息内容过滤和权限控制
    var filteredMsgs []*sdkws.MsgData
    for _, msg := range msgs {
        // 处理删除和撤回消息
        if msg.Status == constant.MsgStatusHasDeleted {
            // 已删除消息,根据用户权限决定是否显示
            if userID == msg.SendID {
                // 发送者可以看到删除标记
                filteredMsgs = append(filteredMsgs, msg)
            }
            // 其他用户看不到已删除消息
            continue
        }

        // 处理引用消息的权限
        if msg.ContentType == constant.Quote {
            db.handlerQuote(ctx, userID, conversationID, []*model.MsgInfoModel{msg})
        }

        filteredMsgs = append(filteredMsgs, msg)
    }

    return filteredMsgs, nil
}

6.4 序列号缓存管理

6.4.1 GetMinSeq实现
// GetMinSeq 获取会话最小序列号
// 优先从Redis缓存获取,缓存未命中时从MongoDB查询并更新缓存
func (db *seqConversationCache) GetMinSeq(ctx context.Context, conversationID string) (int64, error) {
    // 1. 尝试从Redis缓存获取
    cacheKey := fmt.Sprintf("seq:conversation:min:%s", conversationID)
    minSeq, err := db.rdb.Get(ctx, cacheKey).Int64()
    if err == nil {
        return minSeq, nil // 缓存命中
    }

    // 2. 缓存未命中,从MongoDB查询
    seqData, err := db.msgDatabase.GetMinMaxSeqMongo(ctx, conversationID)
    if err != nil {
        return 0, err
    }

    // 3. 更新Redis缓存,设置过期时间
    err = db.rdb.Set(ctx, cacheKey, seqData.MinSeq, time.Hour*24).Err()
    if err != nil {
        log.ZWarn(ctx, "SetMinSeq cache failed", err, "conversationID", conversationID)
    }

    return seqData.MinSeq, nil
}
6.4.2 GetMaxSeq实现
// GetMaxSeq 获取会话最大序列号
// 实时性要求高,优先从Redis获取最新值
func (db *seqConversationCache) GetMaxSeq(ctx context.Context, conversationID string) (int64, error) {
    // 1. 从Redis获取实时最大序列号
    cacheKey := fmt.Sprintf("seq:conversation:max:%s", conversationID)
    maxSeq, err := db.rdb.Get(ctx, cacheKey).Int64()
    if err == nil {
        return maxSeq, nil
    }

    // 2. Redis中不存在,从MongoDB查询并缓存
    seqData, err := db.msgDatabase.GetMinMaxSeqMongo(ctx, conversationID)
    if err != nil {
        return 0, err
    }

    // 3. 设置缓存,MaxSeq缓存时间较短,保证实时性
    err = db.rdb.Set(ctx, cacheKey, seqData.MaxSeq, time.Minute*30).Err()
    if err != nil {
        log.ZWarn(ctx, "SetMaxSeq cache failed", err)
    }

    return seqData.MaxSeq, nil
}

6.5 MongoDB消息查询实现

6.5.1 GetMessageBySeqs核心逻辑
// GetMessageBySeqs 从MongoDB按序列号批量查询消息
// 路径:open-im-server/pkg/common/storage/database/mongo/msg.go
func (m *MsgMongo) GetMessageBySeqs(ctx context.Context, conversationID string, userID string, seqs []int64) ([]*sdkws.MsgData, error) {
    // 1. 计算文档范围,MongoDB中消息按固定数量分片存储
    if len(seqs) == 0 {
        return nil, nil
    }

    // 2. 按文档分组序列号
    docSeqsMap := m.calculateDocumentRanges(conversationID, seqs)
    
    var allMsgs []*sdkws.MsgData
    
    // 3. 逐个文档查询消息
    for docID, docSeqs := range docSeqsMap {
        msgs, err := m.findMsgsByDocumentAndSeqs(ctx, docID, docSeqs)
        if err != nil {
            log.ZError(ctx, "findMsgsByDocumentAndSeqs failed", err, "docID", docID)
            continue
        }
        allMsgs = append(allMsgs, msgs...)
    }

    // 4. 按序列号排序
    sort.Slice(allMsgs, func(i, j int) bool {
        return allMsgs[i].Seq < allMsgs[j].Seq
    })

    return allMsgs, nil
}

// calculateDocumentRanges 计算序列号对应的文档范围
func (m *MsgMongo) calculateDocumentRanges(conversationID string, seqs []int64) map[string][]int64 {
    docSeqsMap := make(map[string][]int64)
    
    for _, seq := range seqs {
        // 计算序列号对应的文档ID
        // MongoDB中每个文档存储固定数量的消息(如10000条)
        docIndex := (seq - 1) / 10000  // 文档索引
        docID := fmt.Sprintf("%s:%d", conversationID, docIndex)
        
        docSeqsMap[docID] = append(docSeqsMap[docID], seq)
    }
    
    return docSeqsMap
}
6.5.2 FindSeqs核心查询
// findMsgsByDocumentAndSeqs 在指定文档中查找特定序列号的消息
func (m *MsgMongo) findMsgsByDocumentAndSeqs(ctx context.Context, docID string, seqs []int64) ([]*sdkws.MsgData, error) {
    // 1. 构建MongoDB查询条件
    filter := bson.M{
        "_id": docID,  // 文档ID
        "msgs": bson.M{
            "$elemMatch": bson.M{
                "msg.seq": bson.M{"$in": seqs}, // 序列号在指定列表中
            },
        },
    }

    // 2. 投影字段,只返回匹配的消息
    projection := bson.M{
        "msgs.$": 1, // 只返回匹配条件的数组元素
    }

    // 3. 执行查询
    cursor, err := m.coll.Find(ctx, filter, options.Find().SetProjection(projection))
    if err != nil {
        return nil, err
    }
    defer cursor.Close(ctx)

    var docs []MsgDocModel
    if err = cursor.All(ctx, &docs); err != nil {
        return nil, err
    }

    // 4. 转换为消息数据格式
    var msgs []*sdkws.MsgData
    for _, doc := range docs {
        for _, msgInfo := range doc.Msgs {
            if utils.Contain(msgInfo.Msg.Seq, seqs) {
                msgs = append(msgs, msgInfo.Msg)
            }
        }
    }

    return msgs, nil
}

6.6 消息内容过滤处理

6.6.1 handlerDeleteAndRevoked删除和撤回消息处理
// handlerDeleteAndRevoked 处理删除和撤回消息的显示逻辑
// 路径:open-im-server/pkg/common/storage/controller/msg.go:773-817
func (db *commonMsgDatabase) handlerDeleteAndRevoked(ctx context.Context, userID string, msgs []*model.MsgInfoModel) {
    for _, msgInfo := range msgs {
        msg := msgInfo.Msg
        
        // 处理已删除消息
        if msg.Status == constant.MsgStatusHasDeleted {
            if userID == msg.SendID {
                // 发送者可以看到删除提示:"你删除了一条消息"
                msg.Content = `{"text":"你删除了一条消息"}`
                msg.ContentType = constant.Text
            } else {
                // 接收者看不到已删除消息,从结果中移除
                continue
            }
        }

        // 处理撤回消息
        if msg.ContentType == constant.Revoke {
            var revokeContent struct {
                RevokerID       string `json:"revokerID"`
                RevokerNickname string `json:"revokerNickname"`
                RevokeTime      int64  `json:"revokeTime"`
                SourceMsgSendID string `json:"sourceMsgSendID"`
                SessionType     int32  `json:"sessionType"`
            }
            
            if err := json.Unmarshal([]byte(msg.Content), &revokeContent); err != nil {
                continue
            }
            
            // 根据撤回者和查看者的关系显示不同文案
            if revokeContent.RevokerID == userID {
                msg.Content = `{"text":"你撤回了一条消息"}`
            } else if revokeContent.SourceMsgSendID == userID {
                msg.Content = fmt.Sprintf(`{"text":"%s撤回了一条消息"}`, revokeContent.RevokerNickname)
            } else {
                // 群聊中其他人的撤回消息
                msg.Content = fmt.Sprintf(`{"text":"%s撤回了一条消息"}`, revokeContent.RevokerNickname)
            }
            msg.ContentType = constant.Text
        }
    }
}
6.6.2 handlerQuote引用消息处理
// handlerQuote 处理引用消息的权限和内容显示
// 路径:open-im-server/pkg/common/storage/controller/msg.go:818-824
func (db *commonMsgDatabase) handlerQuote(ctx context.Context, userID, conversationID string, msgs []*model.MsgInfoModel) {
    for _, msgInfo := range msgs {
        msg := msgInfo.Msg
        
        if msg.ContentType != constant.Quote {
            continue
        }
        
        // 解析引用消息内容
        var quoteContent struct {
            Text         string          `json:"text"`
            QuoteMessage *sdkws.MsgData  `json:"quoteMessage"`
        }
        
        if err := json.Unmarshal([]byte(msg.Content), &quoteContent); err != nil {
            log.ZError(ctx, "unmarshal quote content failed", err)
            continue
        }
        
        // 检查被引用消息的权限
        if quoteContent.QuoteMessage != nil {
            quotedMsg := quoteContent.QuoteMessage
            
            // 检查用户是否有权限查看被引用的消息
            if !db.hasPermissionToViewMessage(ctx, userID, conversationID, quotedMsg) {
                // 无权限查看,显示提示文案
                quoteContent.QuoteMessage = &sdkws.MsgData{
                    Content:     `{"text":"该消息已被删除"}`,
                    ContentType: constant.Text,
                    SendTime:    quotedMsg.SendTime,
                    Seq:         quotedMsg.Seq,
                }
            }
            
            // 更新消息内容
            updatedContent, _ := json.Marshal(quoteContent)
            msg.Content = string(updatedContent)
        }
    }
}

// hasPermissionToViewMessage 检查用户是否有权限查看特定消息
func (db *commonMsgDatabase) hasPermissionToViewMessage(ctx context.Context, userID, conversationID string, msg *sdkws.MsgData) bool {
    // 1. 检查消息是否已被删除
    if msg.Status == constant.MsgStatusHasDeleted {
        return userID == msg.SendID // 只有发送者可以看到自己删除的消息
    }
    
    // 2. 检查消息是否已被撤回
    if msg.ContentType == constant.Revoke {
        return true // 撤回消息所有人都可以看到撤回提示
    }
    
    // 3. 检查时间权限(是否在用户加入会话之后)
    userJoinTime, err := db.getUserJoinTime(ctx, userID, conversationID)
    if err == nil && msg.SendTime < userJoinTime {
        return false // 加入之前的消息无权查看
    }
    
    return true // 默认有权限
}

6.7 云端消息拉取完整流程图

graph TD
    A[客户端发起PullMessageBySeqList请求] --> B[服务端PullMessageBySeqs处理]
    
    B --> C[初始化响应结构]
    C --> D[遍历序列号范围请求]
    
    D --> E{判断会话类型}
    E -->|普通会话| F[获取用户会话信息]
    E -->|通知会话| G[构建序列号列表]
    
    F --> H[验证用户权限]
    H --> I[调用GetMsgBySeqsRange]
    
    G --> J[调用GetMsgBySeqs]
    
    I --> K[数据库层getMsgBySeqs]
    J --> K
    
    K --> L[获取MinSeq边界]
    L --> M[获取MaxSeq边界]
    M --> N[过滤有效序列号]
    
    N --> O{序列号缓存}
    O -->|缓存命中| P[从Redis获取边界]
    O -->|缓存未命中| Q[从MongoDB查询边界]
    
    P --> R[调用GetMessageBySeqs]
    Q --> S[更新Redis缓存]
    S --> R
    
    R --> T[计算文档范围]
    T --> U[按文档分组序列号]
    U --> V[逐个文档查询]
    
    V --> W[FindSeqs执行MongoDB查询]
    W --> X[构建查询条件]
    X --> Y[执行find操作]
    Y --> Z[投影匹配字段]
    
    Z --> AA[获取原始消息数据]
    AA --> BB[handlerDeleteAndRevoked]
    BB --> CC[handlerQuote]
    
    CC --> DD{消息内容过滤}
    DD -->|已删除消息| EE[权限检查和内容替换]
    DD -->|撤回消息| FF[生成撤回提示文案]
    DD -->|引用消息| GG[检查被引用消息权限]
    DD -->|普通消息| HH[保持原始内容]
    
    EE --> II[合并过滤结果]
    FF --> II
    GG --> II
    HH --> II
    
    II --> JJ[按序列号排序]
    JJ --> KK[判断拉取边界]
    
    KK --> LL{拉取顺序}
    LL -->|升序| MM[检查maxSeq <= endSeq]
    LL -->|降序| NN[检查beginSeq <= minSeq]
    
    MM --> OO[设置isEnd标志]
    NN --> OO
    
    OO --> PP[构建PullMsgs响应]
    PP --> QQ[添加到响应映射]
    
    QQ --> RR{是否处理完所有会话}
    RR -->|否| D
    RR -->|是| SS[返回完整响应]
    
    SS --> TT[客户端接收响应]
    TT --> UU[触发消息处理流程]
    
    style A fill:#e1f5fe
    style TT fill:#e8f5e8
    style K fill:#fff3e0
    style W fill:#fff3e0
    style DD fill:#ffebee

这个详细的实现解析涵盖了从客户端请求到服务端响应的完整流程,包括缓存机制、数据库查询、权限控制和内容过滤等关键环节。每个步骤都考虑了性能优化和数据安全,确保消息同步的准确性和高效性。