OpenIM 源码深度解析系列(六):单聊核心存储结构全景解析

469 阅读16分钟

单聊核心存储结构全景解析

📋 概述

OpenIM 单聊功能采用客户端-服务端分离的存储架构:

  • 客户端存储:基于 SQLite 的本地数据库,实现快速查询和离线访问
  • 服务端存储:基于 MongoDB + Redis + Kafka 的分布式存储,保证数据一致性和高可用

为了更好地理解存储结构的设计原理,我们首先通过流程图来分析单聊消息的完整生命周期,然后深入解析每个阶段涉及的存储结构和数据流转。

🔄 消息生命周期与数据流转

在深入分析存储结构之前,我们需要理解单聊消息的完整生命周期。从用户输入文字到对方看到消息并产生已读回执,每个环节都涉及到不同层次的数据存储和状态管理。下面的流程图将帮助我们理解存储设计的业务背景。

1. 消息存储状态变迁流程

stateDiagram-v2
    [*] --> Creating : 用户输入
    Creating --> Sending : 创建消息对象
    Sending --> Processing : 发送到服务端
    Processing --> Queued : 投递到Kafka
    Queued --> SeqAllocated : MsgTransfer分配Seq
    SeqAllocated --> Stored : 存储到MongoDB
    Stored --> Pushed : 推送给接收者
    Pushed --> Received : 接收者收到
    Received --> Read : 用户查看
    Read --> Acknowledged : 发送已读回执
    Acknowledged --> [*] : 流程完成
    
    state Creating {
        ClientMsgID_生成
        Status_MsgStatusSending
        Seq_0
        IsRead_false
    }
    
    state Sending {
        本地数据库保存
        WebSocket发送
        等待服务端响应
    }
    
    state Processing {
        生成ServerMsgID
        设置SendTime
        校验权限和设置
    }
    
    state SeqAllocated {
        Redis分配序号
        发送者自动已读
        MaxSeq更新
        ReadSeq更新
    }
    
    state Received {
        接收者MaxSeq更新
        计算未读数
        显示红点提醒
    }
    
    state Read {
        用户查看消息
        ReadSeq更新到MaxSeq
        红点消失
    }
    
    state Acknowledged {
        IsRead设置为true
        已读时间戳记录
        多端状态同步
    }

2. 存储层红点逻辑实现

flowchart TD
    A[消息事件触发] --> B{消息类型判断}
    
    B -->|新消息到达| C[接收者处理]
    B -->|发送消息| D[发送者处理]
    B -->|查看消息| E[已读处理]
    B -->|同步事件| F[多端同步]
    
    C --> C1[MaxSeq +1]
    C1 --> C2[计算未读数]
    C2 --> C3{未读数 > 0?}
    C3 -->|是| C4[显示红点]
    C3 -->|否| C5[隐藏红点]
    
    D --> D1[MaxSeq +1]
    D1 --> D2[ReadSeq +1]
    D2 --> D3[未读数 = 0]
    D3 --> D4[不显示红点]
    
    E --> E1[ReadSeq = MaxSeq]
    E1 --> E2[未读数 = 0]
    E2 --> E3[红点消失]
    E3 --> E4[发送已读回执]
    
    F --> F1[同步MaxSeq和ReadSeq]
    F1 --> F2[重新计算未读数]
    F2 --> F3[更新红点状态]
    
    C4 --> G[UI更新]
    C5 --> G
    D4 --> G
    E3 --> G
    F3 --> G
    
    G --> H[用户看到最新状态]

3. 跨端数据同步存储机制

sequenceDiagram
    participant A as 设备A
    participant Server as 消息服务器
    participant B as 设备B
    participant C as 设备C
    participant Redis as Redis缓存
    participant DB as MongoDB
    
    Note over A,DB: 场景1:设备A发送消息
    A->>Server: 发送消息
    Server->>Redis: 分配Seq=100
    Server->>DB: 存储消息
    Server->>Redis: 设置A的ReadSeq=100
    
    par 同步到其他设备
        Server->>B: 推送新消息(Seq=100)
        Server->>C: 推送新消息(Seq=100)
    end
    
    B->>B: MaxSeq=100, ReadSeq=99<br/>未读数=1, 显示红点
    C->>C: MaxSeq=100, ReadSeq=99<br/>未读数=1, 显示红点
    
    Note over A,DB: 场景2:设备B查看消息
    B->>B: 用户查看消息
    B->>Server: 标记已读(ReadSeq=100)
    Server->>Redis: 更新B的ReadSeq=100
    
    par 同步已读状态
        Server->>A: 已读回执通知
        Server->>C: 已读位置同步
    end
    
    A->>A: 更新消息IsRead=true<br/>显示"已读"标识
    C->>C: ReadSeq=100<br/>未读数=0, 红点消失
    
    Note over A,DB: 场景3:设备C离线后上线
    C->>Server: 请求消息同步
    Server->>Redis: 获取C的ReadSeq=95
    Server->>DB: 查询Seq>95的消息
    Server->>C: 返回增量消息
    
    C->>C: 批量处理消息<br/>更新MaxSeq=100<br/>计算未读数=5<br/>显示红点
    
    Note over A,DB: 场景4:全量数据一致性保证
    loop 定期同步检查
        Server->>Redis: 检查ReadSeq一致性
        Server->>DB: 验证消息完整性
        Server->>A: 推送缺失消息
        Server->>B: 推送缺失消息
        Server->>C: 推送缺失消息
    end

4. 红点计算的存储设计原理

红点计算公式

OpenIM的红点机制基于精确的数学公式:

未读数 = MaxSeq(会话最大序号) - ReadSeq(用户已读序号)
双重已读状态设计

OpenIM采用双重已读机制确保精确控制:

1. 会话级别已读(ReadSeq)
  • 作用:控制红点显示和未读计数
  • 存储:Redis缓存 + MongoDB持久化
  • 更新时机:用户查看消息、发送消息时
  • 影响范围:整个会话的未读数统计
2. 消息级别已读(IsRead)
  • 作用:精确标记单条消息的已读状态
  • 存储:消息表的isRead字段 + AttachedInfo的HasReadTime
  • 更新时机:用户查看消息、收到已读回执时
  • 影响范围:单条消息的状态显示(如已读标识)
红点状态变化场景
场景MaxSeq变化ReadSeq变化红点结果说明
接收新消息+1不变出现红点 ✅正常的未读提醒
查看消息不变设置为MaxSeq红点消失 ✅已读后清除红点
发送消息+1+1无红点 ✅自己发送不产生红点
多端同步同步同步一致显示 ✅多设备状态一致

以上流程图展示了单聊消息从创建到读取的完整数据流转过程。为了支撑这些复杂的业务逻辑,OpenIM设计了精密的存储架构。接下来我们将详细解析每一层存储结构的设计原理和数据模型。


第一部分:客户端存储层(SQLite)

🗄️ 核心表结构概览

OpenIM SDK 使用三个核心表实现完整的即时通讯数据存储:

表名作用数据特点查询频率
LocalConversation会话管理相对稳定,变更较少高频查询
LocalChatLog消息存储数据量大,增长快速超高频查询
LocalSendingMessages发送状态临时数据,自动清理中频查询

📋 1. LocalConversation - 本地会话表

表名: local_conversations

完整字段结构
字段名字段类型索引详细描述
ConversationIDstring(128) [主键]PRIMARY KEY会话唯一标识
• 单聊格式:si_${userID1}_${userID2}
• 群聊格式:sg_${groupID}n_${groupID}(通知群)
• 作为主键,关联消息表
ConversationTypeint32会话类型枚举
1 = 单聊会话 (SINGLE_CHAT)
2 = 群聊会话 (GROUP_CHAT)
3 = 超级群聊 (SUPER_GROUP_CHAT)
4 = 通知会话 (NOTIFICATION)
UserIDstring(64)单聊对方用户ID
• 快速定位聊天对象
• 群聊中此字段为空
GroupIDstring(128)群聊群组ID
• 群聊会话中的群组标识
• 单聊中此字段为空
ShowNamestring(255)会话显示名称
• 对方昵称或备注名
• 群名称
• 影响会话列表显示
FaceURLstring(255)会话头像URL
• 对方头像地址
• 群头像地址
• 支持本地缓存
RecvMsgOptint32接收消息选项枚举
0 = 正常接收 (NORMAL)
1 = 不接收消息 (NOT_RECEIVE)
2 = 接收但不提醒 (NOT_NOTIFY)
UnreadCountint32未读消息数量
• 实时更新计数
• 避免实时统计查询
• 范围:0 到 999+
GroupAtTypeint32群组@类型枚举
0 = 无@ (NO_AT)
1 = @所有人 (AT_ALL)
2 = @我 (AT_ME)
3 = @所有人且@我 (AT_ALL_AND_ME)
LatestMsgstring(1000)最新消息JSON
• 会话列表预览
• 提升性能
• JSON格式存储完整消息结构
LatestMsgSendTimeint64INDEX最新消息时间戳
• 会话列表排序依据
• 毫秒级时间戳
• 关键索引字段
DraftTextstring草稿文本内容
• 用户输入但未发送的文本
• 支持富文本格式
DraftTextTimeint64草稿文本时间戳
• 草稿创建/更新时间
• 用于草稿过期清理
IsPinnedbool是否置顶
• true = 置顶会话优先显示
• false = 正常排序
IsPrivateChatbool是否私密聊天
• true = 阅后即焚功能开启
• false = 正常聊天模式
BurnDurationint32阅后即焚时长
• 默认30秒
• 范围:10-604800秒(7天)
• 仅在IsPrivateChat=true时生效
IsNotInGroupbool是否不在群组中
• true = 已退群但保留会话
• false = 正常在群状态
• 仅群聊会话有效
UpdateUnreadCountTimeint64未读数更新时间戳
• 未读数最后更新时间
• 用于增量同步优化
AttachedInfostring(1024)附加信息
• 扩展业务数据
• JSON格式存储
Exstring(1024)扩展字段
• 自定义业务数据
• 预留扩展能力
MaxSeqint64最大消息序列号
• 增量同步边界
• 服务端同步使用
MinSeqint64最小消息序列号
• 清理边界控制
• 历史消息管理
MsgDestructTimeint64消息销毁时间
• 默认604800秒(7天)
• 消息自动清理时间
• 范围:3600-31536000秒(1小时-1年)
IsMsgDestructbool是否开启消息销毁
• true = 开启自动销毁
• false = 永久保存
• 默认false

📨 2. LocalChatLog - 本地聊天记录表

表名: local_chat_logs

完整字段结构
字段名字段类型索引详细描述
ClientMsgIDstring(64) [主键]PRIMARY KEY客户端消息ID
• UUID格式,客户端生成
• 消息去重和状态跟踪
• 格式:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ServerMsgIDstring(64)服务端消息ID
• 服务器分配的全局唯一ID
• 多端同步保证
• 消息投递后由服务端返回
SendIDstring(64)发送者用户ID
• 消息归属和权限判断
• 与用户表关联
RecvIDstring(64)INDEX接收者ID
• 重要索引字段
• 单聊为对方用户ID
• 群聊为群组ID
SenderPlatformIDint32发送者平台ID枚举
1 = iOS
2 = Android
3 = Windows
4 = OSX
5 = Web
6 = 小程序
7 = Linux
8 = iPad
9 = Android Pad
SenderNicknamestring(255)发送者昵称快照
• 避免改名后显示异常
• 历史消息显示使用
SenderFaceURLstring(255)发送者头像快照
• 保持历史一致性
• 防止头像变更影响历史显示
SessionTypeint32会话类型枚举
1 = 单聊消息 (SINGLE_CHAT)
2 = 群聊消息 (GROUP_CHAT)
3 = 超级群聊 (SUPER_GROUP_CHAT)
4 = 通知消息 (NOTIFICATION)
MsgFromint32消息来源枚举
100 = 用户发送 (USER)
200 = 系统通知 (ADMIN)
300 = 机器人 (BOT)
ContentTypeint32INDEX消息内容类型枚举
101 = 文本消息 (TEXT)
102 = 图片消息 (PICTURE)
103 = 语音消息 (SOUND)
104 = 视频消息 (VIDEO)
105 = 文件消息 (FILE)
106 = @消息 (AT_TEXT)
107 = 合并转发 (MERGE)
108 = 名片消息 (CARD)
109 = 位置消息 (LOCATION)
110 = 自定义消息 (CUSTOM)
111 = 撤回消息通知 (REVOKE)
112 = 阅读已读回执 (C2C_READ_RECEIPT)
113 = 输入状态 (TYPING)
114 = 引用消息 (QUOTE)
115 = 表情回复 (FACE)
116 = 高级文本 (ADVANCED_TEXT)
系统通知类型(1000+)
1001 = 好友申请 (FRIEND_APPLICATION_APPROVED)
1002 = 好友申请被拒绝
1003 = 好友被删除
1004 = 群创建通知
1005 = 群信息变更
1006 = 群成员邀请
1007 = 群成员移除
1008 = 群解散通知
1009 = 用户入群申请
1010 = 用户退群通知
Contentstring(1000)消息内容JSON
• 根据ContentType解析
• 支持复杂数据结构
• 各类型具体格式见下文
IsReadbool是否已读标记
• true = 已读消息
• false = 未读消息
• 影响未读数统计
Statusint32消息发送状态枚举
1 = 发送中 (SENDING)
2 = 发送成功 (SEND_SUCCESS)
3 = 发送失败 (SEND_FAILURE)
4 = 已删除 (DELETED)
5 = 已撤回 (REVOKED)
Seqint64INDEX消息序列号
• 服务器分配,单调递增
• 消息排序和同步依据
• 会话级别唯一
SendTimeint64INDEX发送时间戳
• 毫秒级精度
• 支持时间范围查询
• 消息排序依据
CreateTimeint64创建时间戳
• 消息在本地数据库创建时间
• 用于本地排序和清理
AttachedInfostring(1024)附加信息
• 扩展业务数据
• 已读时间、反应信息等
Exstring(1024)扩展字段
• 自定义业务数据
• 预留扩展能力
LocalExstring(1024)本地扩展字段
• 仅客户端使用
• 不同步到服务端
• 本地状态存储

🚀 3. LocalSendingMessages - 发送状态表

表名: local_sending_messages

完整字段结构
字段名字段类型索引详细描述
ConversationIDstring(128) [联合主键]PRIMARY KEY会话标识
• 批量处理发送中消息
• 与LocalConversation表关联
ClientMsgIDstring(64) [联合主键]PRIMARY KEY客户端消息ID
• 定位具体发送中消息
• 与LocalChatLog表关联
Exstring(1024)扩展信息
• 重发次数、失败原因等
• JSON格式存储发送状态详情
• 包含:重试次数、最后错误码、下次重试时间等

第二部分:服务端存储层

🏗️ 架构概览

服务端采用三层存储架构:

  • MongoDB:持久化存储,保证数据安全
  • Redis:高速缓存,提升查询性能
  • Kafka:异步消息队列,实现系统解耦

🗄️ MongoDB 数据库存储层

1. Conversation - 会话核心表

集合名: conversation

完整字段结构
字段名字段类型索引详细描述
_idObjectIdPRIMARY KEYMongoDB主键
• 自动生成的唯一标识
OwnerUserIDstringINDEX会话拥有者ID
• 每个用户都有自己的会话副本
• 多端同步的基础
• 复合索引:owner_user_id + conversation_id
ConversationIDstringINDEX会话唯一标识
• 单聊格式:si_${userID1}_${userID2}
• 群聊格式:sg_${groupID}
• 通知格式:n_${groupID}
• 与客户端保持一致
ConversationTypeint32会话类型枚举
1 = 单聊会话 (SINGLE_CHAT)
2 = 群聊会话 (GROUP_CHAT)
3 = 超级群聊 (SUPER_GROUP_CHAT)
4 = 通知会话 (NOTIFICATION)
UserIDstring单聊对方用户ID
• 单聊中的另一方用户
• 群聊中为空
GroupIDstring群聊群组ID
• 群聊会话中的群组标识
• 单聊中为空
RecvMsgOptint32消息接收选项枚举
0 = 正常接收 (NORMAL)
1 = 不接收消息 (NOT_RECEIVE)
2 = 接收不提醒 (NOT_NOTIFY)
IsPinnedbool是否置顶
• true = 置顶会话
• false = 正常会话
• 影响会话列表排序
IsPrivateChatbool是否私密聊天
• true = 阅后即焚功能开启
• false = 正常聊天模式
BurnDurationint32阅后即焚时长
• 单位:秒
• 默认:30秒
• 范围:10-604800秒
GroupAtTypeint32群组@类型枚举
0 = 无@ (NO_AT)
1 = @所有人 (AT_ALL)
2 = @我 (AT_ME)
3 = @所有人且@我 (AT_ALL_AND_ME)
AttachedInfostring附加信息
• 扩展业务数据JSON字符串
Exstring扩展字段
• 自定义业务数据
MaxSeqint64最大消息序列号
• 增量同步使用
• 与SeqConversation表同步
MinSeqint64最小消息序列号
• 清理边界控制
• 历史消息管理
CreateTimetime.Time创建时间
• MongoDB时间类型
• 会话创建时间戳
IsMsgDestructbool是否开启消息销毁
• true = 开启自动销毁
• false = 永久保存
MsgDestructTimeint64消息销毁时长
• 单位:秒
• 默认:604800(7天)
LatestMsgDestructTimetime.Time最新消息销毁时间
• 用于批量清理消息
2. SeqConversation - 会话序列号表

集合名: seq

完整字段结构
字段名字段类型索引详细描述
_idObjectIdPRIMARY KEYMongoDB主键
ConversationIDstringUNIQUE INDEX会话标识
• 唯一索引,快速定位序列号信息
• 与会话表关联的关键字段
MaxSeqint64最大序列号
• 新消息seq分配依据
• 原子递增操作保证唯一性
• 初始值:0
MinSeqint64最小序列号
• 历史消息边界控制
• 消息清理使用
• 初始值:0
3. SeqUser - 用户序列号表

集合名: seq_user

完整字段结构
字段名字段类型索引详细描述
_idObjectIdPRIMARY KEYMongoDB主键
UserIDstringINDEX用户ID
• 多端同步标识
• 复合索引:user_id + conversation_id
ConversationIDstringINDEX会话ID
• 多会话管理
• 与SeqConversation表关联
MinSeqint64用户最小序列号
• 增量拉取边界
• 用户侧消息清理边界
• 初始值:0
MaxSeqint64用户最大序列号
• 断点续传使用
• 标识用户已同步的最大seq
• 初始值:0
ReadSeqint64已读序列号
• 未读数计算公式:MaxSeq - ReadSeq
• 已读回执功能基础
• 初始值:0
4. 消息存储结构
MsgDocModel - 消息文档模型

集合名: msg

文档结构
{
  "_id": ObjectId,
  "doc_id": "会话ID:文档索引",
  "msgs": [
    {
      "msg": MsgDataModel,
      "revoke": RevokeModel,
      "del_list": ["删除用户ID列表"],
      "is_read": boolean
    }
  ]
}
MsgInfoModel - 消息信息模型
字段名字段类型详细描述
msgMsgDataModel消息主体数据
• 包含完整消息内容和元数据
• 详细结构见MsgDataModel
revokeRevokeModel撤回信息
• 消息被撤回时不为空
• 正常消息为null
• 详细结构见RevokeModel
del_list[]string删除用户列表
• 记录删除此消息的用户ID数组
• 单聊中最多包含2个用户ID
• 实现"删除消息"功能,不同用户可独立删除
is_readbool全员已读标记
• true = 消息已被所有接收者读取
• false = 仍有用户未读
• 单聊中表示对方是否已读
RevokeModel - 撤回信息模型
字段名字段类型详细描述
roleint32撤回者角色枚举
1 = 普通用户 (NORMAL_USER)
2 = 群管理员 (GROUP_ADMIN)
3 = 群主 (GROUP_OWNER)
100 = 系统管理员 (SYSTEM_ADMIN)
user_idstring撤回者用户ID
• 执行撤回操作的用户
• 撤回权限判断依据
nicknamestring撤回者昵称
• 撤回时的昵称快照
• 用于撤回通知显示
timeint64撤回时间
• 消息被撤回的时间戳(毫秒)
• 用于撤回时效判断
MsgDataModel - 消息数据模型
字段名字段类型详细描述
send_idstring发送者ID
• 对应会话表中的OwnerUserID
• 权限验证和展示
recv_idstring接收者ID
• 单聊为对方用户ID
• 群聊为群组ID
• 消息路由使用
group_idstring群组ID
• 群聊消息的群组标识
• 单聊中为空字符串
conversation_idstring会话标识
• 关联会话表中的ConversationID
• 消息归档和查询索引
client_msg_idstring客户端消息ID
• 与客户端LocalChatLog表对应
• 去重和状态跟踪
• UUID格式
server_msg_idstring服务端消息ID
• 服务端生成的全局唯一ID
• MongoDB ObjectId字符串形式
sender_platform_idint32发送者平台ID枚举
1 = iOS, 2 = Android, 3 = Windows
4 = OSX, 5 = Web, 6 = 小程序
7 = Linux, 8 = iPad, 9 = Android Pad
sender_nicknamestring发送者昵称
• 发送时的昵称快照
• 避免显示异常
sender_face_urlstring发送者头像URL
• 发送时的头像快照
• 历史一致性保证
session_typeint32会话类型
• 与ConversationType对应
1 = 单聊, 2 = 群聊, 3 = 超级群聊, 4 = 通知
msg_fromint32消息来源枚举
100 = 用户发送 (USER)
200 = 系统通知 (ADMIN)
300 = 机器人 (BOT)
content_typeint32内容类型
• 与客户端ContentType完全对应
• 详细枚举见客户端LocalChatLog表
contentstring消息内容
• JSON格式,与客户端保持一致
• 根据content_type解析不同结构
seqint64消息序列号
• 对应会话表中的MaxSeq
• 全局单调递增,排序和同步依据
send_timeint64发送时间
• 与客户端SendTime对应
• 毫秒级时间戳
create_timeint64创建时间
• 服务端创建时间戳
• 用于服务端排序和清理
statusint32消息状态
• 与客户端Status对应
1 = 发送中, 2 = 成功, 3 = 失败
is_readbool是否已读
• 与客户端IsRead对应
• 个人已读状态标记
optionsmap[string]bool消息选项配置
• 详细配置见msgprocessor/options.go
• 控制消息行为:存储、推送、同步等
offline_pushOfflinePushModel离线推送配置
• 推送标题、内容、扩展信息等
at_user_id_list[]string@用户ID列表
• 群聊@功能使用
• 空数组表示无@用户
attached_infostring附加信息
• 扩展业务数据JSON字符串
exstring扩展字段
• 自定义业务数据
OfflinePushModel - 离线推送模型
字段名字段类型详细描述
titlestring推送标题
• 显示在通知栏的标题
• 默认为发送者昵称
descstring推送描述
• 推送内容摘要
• 默认为消息内容预览
exstring推送扩展信息
• 自定义推送数据
ios_push_soundstringiOS推送声音
• iOS通知声音文件名
• 默认为系统声音
ios_badge_countbooliOS角标计数
• true = 显示角标数字
• false = 不显示角标

🚀 Redis 缓存层

OpenIM Redis缓存采用分层过期策略,平衡性能与数据一致性:

1. 会话相关缓存
缓存Key格式数据类型TTL时间缓存内容使用场景
CONVERSATION:{ownerUserID}:{conversationID}Hash12小时完整会话信息会话详情快速查询
CONVERSATION_IDS:{ownerUserID}Set12小时用户所有会话ID集合会话列表加载
CONVERSATION_IDS_HASH:{ownerUserID}String12小时会话ID列表数组json增量同步检测
会话级别消息控制
CONVERSATION_USER_MAX:{userID}Hash12小时用户各会话版本信息增量同步
CONVERSATION_NOT_RECEIVE_MESSAGE_USER_IDS:{conversationID}Set12小时不接收消息的用户集合消息投递过滤
2. 消息相关缓存
缓存Key格式数据类型TTL时间缓存内容使用场景
MSG_CACHE:{conversationID}:{seq}Hash24小时完整消息数据消息快速查询
3. 序列号相关缓存
缓存Key格式数据类型TTL时间缓存内容使用场景
MALLOC_SEQ:{conversationID}:CURRHash1年CURR:会话当前序列号
LAST:会话最后分配序列号
TIME:序列号分配时间戳
新消息seq分配
SEQ_USER_READ:{conversationID}:{userID}String30天用户已读序列号已读回执功能

📨 Kafka 消息队列

核心Topic配置
Topic名称作用分区数副本数分区策略消费者组
toRedisTopic消息到Redis缓存83按conversationID哈希toRedisGroupID
toMongoTopic消息到MongoDB持久化83按conversationID哈希toMongoGroupID
toPushTopic在线用户推送83按userID哈希toPushGroupID
toOfflinePushTopic离线用户推送83按userID哈希toOfflineGroupID