导读
- 阅读本文档需要有一定的后端基础和客户端基础,才能更好地理解本文档的核心内容。
- 本文档的设计是基于后端的视角来撰写可选方案,让客户端更加清楚明白要如何配合后端实现。
- 方案在移动端(Ios、Android、HarmonyOS)和桌面端(Electron、Flutter)都是通用的!!
- 本文档涉及到的相关技术栈有:electron、mysql、sqlite、ts、nestjs、typeorm
- 适用人群:leader、架构师、超级个体
备注:本文着重是方案选型,并非高并发、分布式方案!
一、简介
1.1 背景概述
在即时通讯(IM)应用中,会话管理是核心功能之一,它负责维护用户与联系人或群组之间的对话关系,包括会话列表展示、未读消息数统计、最后一条消息预览等功能。良好的会话管理方案能够显著提升用户体验和应用性能。
1.2 什么是会话
会话(Conversation/Session) 是即时通讯应用中的核心概念,表示一个对话的容器,是用户与联系人之间沟通的桥梁,也是IM应用中用户界面的核心组织单元,主要包括:
- 单聊会话:两个用户之间的对话关系
- 群聊会话:多个用户之间的对话关系
1.3 会话管理核心功能
会话管理的核心功能包括:
- 会话查询: 获取用户的会话列表,支持分页、搜索、过滤等功能,确保数据的高效加载和展示
- 会话创建: 发送或接收消息时自动创建会话,包括单聊和群聊两种场景的会话创建机制
- 会话更新: 收到新消息时更新会话的最后消息和时间,同时处理置顶、免打扰等状态变更
- 会话删除: 用户删除会话时处理,支持软删除(保留历史消息)或硬删除(彻底删除)两种模式
- 会话排序: 按最后消息时间、置顶状态、未读数等维度进行智能排序,优化用户体验
- 未读数计算: 实时统计每个会话的未读消息数,支持多设备同步和离线状态下的准确计数
- 离线同步: 断线重连后同步会话状态,确保数据一致性和完整性
- 多设备同步: 跨设备同步会话状态,包括未读数、置顶状态、最后消息等关键信息
二、会话设计基础
2.1 核心概念
会话(Conversation/Session):
- 指用户与另一个用户(单聊)或多个用户(群聊)之间的对话关系
- 每个会话都有唯一的标识符(sessionId/sid),由服务端生成
- 会话包含: 会话ID、参与者、最后一条消息、未读数、活跃时间等元信息
会话状态:
- 活跃会话: 用户最近有过互动的会话
- 静默会话: 长期没有新消息的会话
- 置顶会话: 用户手动置顶的重要会话
- 删除会话: 用户删除的会话(可能还会保留历史消息)
会话类型:
- 单聊会话: 一对一聊天,参与者固定为2人
- 群聊会话: 多人聊天,参与者可变
- 系统会话: 系统通知、客服机器人等
- 临时会话: 临时创建,可能不持久化
会话生命周期:
- 创建: 发送或接收第一条消息时自动创建
- 活跃: 有新消息交互,保持活跃状态
- 静默: 长期无新消息,进入静默状态
- 归档: 用户手动归档或系统自动归档
- 删除: 用户手动删除会话
会话数据流:
- 消息触发: 会话的创建和更新主要由消息收发触发
- 状态同步: 多设备间需要同步会话状态
- 离线处理: 断线时如何处理会话数据
- 冲突解决: 多设备数据冲突时的处理策略
会话管理核心设计原则:
- 唯一性: 每个会话必须有唯一标识,确保数据一致性
- 实时性: 会话状态需要实时更新,保证用户体验
- 持久化: 会话数据需要可靠存储,支持离线访问
- 同步性: 多设备间需要保持会话状态同步
- 扩展性: 会话系统需要支持各种业务场景的扩展
- 安全性: 会话数据需要保护用户隐私和安全
2.2 会话数据模型设计
2.2.1 核心数据字段
会话数据模型包含以下核心字段:
标识字段(后端生成和存储):
- 会话类型: 单聊、群聊、系统会话等
- 参与者信息: 会话参与者的标识
特殊标识字段(后端生成和存储 / 也有方案是客户端生成):
- 会话ID: 唯一标识符,服务端生成,后端存储
状态字段(后端存储和计算,客户端缓存):
- 最后一条消息: 决定会话排序和展示优先级
- 未读数: 用户未读消息数量,影响用户体验
- 活跃时间: 最后一次交互时间,用于会话排序
状态信息(后端存储,客户端传输):
- 置顶状态: 用户自定义的重要会话标记
- 静音状态: 免打扰设置,不影响未读数但避免打扰
元数据(后端维护):
- 创建时间: 会话创建时间戳
- 更新时间: 最后更新时间戳
- 数据版本: 用于同步和冲突解决的版本号
2.2.2 后端表设计
2.2.2.1 会话表设计(Mysql)
会话管理六表设计方案:为更好地维护会话管理功能,采用多表关联设计方案,各司其职,共同维护会话状态。
会话基本信息表 (session_base_info):
CREATE TABLE session_base_info (
session_id VARCHAR(64) PRIMARY KEY COMMENT '会话ID,服务端生成,唯一标识符',
session_type TINYINT NOT NULL COMMENT '会话类型:1-单聊,2-群聊,3-系统',
last_message_id VARCHAR(64) COMMENT '最后一条消息ID',
last_message_content TEXT COMMENT '最后一条消息内容预览',
last_message_type TINYINT COMMENT '最后一条消息类型',
last_message_time BIGINT COMMENT '最后一条消息时间戳',
created_at BIGINT NOT NULL COMMENT '创建时间',
updated_at BIGINT NOT NULL COMMENT '更新时间',
version INT DEFAULT 0 COMMENT '数据版本号(用于乐观锁)',
INDEX idx_session_type (session_type),
INDEX idx_last_message_time (last_message_time DESC),
INDEX idx_updated_at (updated_at DESC)
) COMMENT '会话基本信息表,存储会话核心元数据';
单聊扩展表 (session_private_ext):
CREATE TABLE session_private_ext (
session_id VARCHAR(64) PRIMARY KEY COMMENT '会话ID,引用session_base_info',
user_id1 BIGINT NOT NULL COMMENT '用户1 ID(较小的用户ID)',
user_id2 BIGINT NOT NULL COMMENT '用户2 ID(较大的用户ID)',
created_at BIGINT NOT NULL COMMENT '创建时间',
updated_at BIGINT NOT NULL COMMENT '更新时间',
INDEX idx_user_id1 (user_id1),
INDEX idx_user_id2 (user_id2),
INDEX idx_user_pair (user_id1, user_id2),
FOREIGN KEY (session_id) REFERENCES session_base_info(session_id) ON DELETE CASCADE
) COMMENT '单聊扩展表,存储单聊特有信息';
群聊扩展表 (session_group_ext):
CREATE TABLE session_group_ext (
session_id VARCHAR(64) PRIMARY KEY COMMENT '会话ID,引用session_base_info',
group_name VARCHAR(100) NOT NULL COMMENT '群组名称',
group_avatar VARCHAR(500) COMMENT '群头像URL',
announcement TEXT COMMENT '群公告内容',
max_members INT DEFAULT 500 COMMENT '最大成员数限制',
join_permission TINYINT DEFAULT 1 COMMENT '加入权限:1-需审核,2-自由加入',
message_permission TINYINT DEFAULT 1 COMMENT '发言权限:1-全员,2-仅管理员',
created_at BIGINT NOT NULL COMMENT '创建时间',
updated_at BIGINT NOT NULL COMMENT '更新时间',
INDEX idx_group_name (group_name),
FOREIGN KEY (session_id) REFERENCES session_base_info(session_id) ON DELETE CASCADE
) COMMENT '群聊扩展表,存储群聊特有信息';
会话成员表 (session_members):
CREATE TABLE session_members (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
session_id VARCHAR(64) NOT NULL COMMENT '会话ID,引用session_base_info',
user_id BIGINT NOT NULL COMMENT '成员用户ID',
role TINYINT DEFAULT 1 COMMENT '角色:1-普通成员,2-管理员,3-群主',
nickname VARCHAR(50) COMMENT '群内昵称',
unread_count INT DEFAULT 0 COMMENT '该成员在本会话的未读消息数',
is_top BOOLEAN DEFAULT FALSE COMMENT '该成员是否置顶此会话',
is_muted BOOLEAN DEFAULT FALSE COMMENT '该成员是否静音此会话',
is_deleted BOOLEAN DEFAULT FALSE COMMENT '该成员是否删除此会话(软删除)',
join_time BIGINT NOT NULL COMMENT '加入时间',
is_active BOOLEAN DEFAULT TRUE COMMENT '是否活跃成员',
created_at BIGINT NOT NULL COMMENT '创建时间',
updated_at BIGINT NOT NULL COMMENT '更新时间',
UNIQUE KEY uk_session_user (session_id, user_id),
INDEX idx_user_id (user_id),
INDEX idx_session_id (session_id),
INDEX idx_role (role),
INDEX idx_is_top (is_top),
INDEX idx_is_muted (is_muted),
FOREIGN KEY (session_id) REFERENCES session_base_info(session_id) ON DELETE CASCADE
) COMMENT '会话成员表,存储用户与会话的关系及个人设置';
会话操作日志表 (session_operation_logs):
CREATE TABLE session_operation_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
session_id VARCHAR(64) NOT NULL COMMENT '会话ID,引用session_base_info',
operator_id BIGINT NOT NULL COMMENT '操作者用户ID',
operation_type TINYINT NOT NULL COMMENT '操作类型:1-置顶,2-取消置顶,3-静音,4-取消静音,5-删除,6-恢复删除,7-创建会话',
old_value TEXT COMMENT '操作前值(JSON格式)',
new_value TEXT COMMENT '操作后值(JSON格式)',
operation_time BIGINT NOT NULL COMMENT '操作时间戳',
ip_address VARCHAR(45) COMMENT '操作IP地址',
device_info VARCHAR(200) COMMENT '设备信息',
created_at BIGINT NOT NULL COMMENT '创建时间',
INDEX idx_session_id (session_id),
INDEX idx_operator_id (operator_id),
INDEX idx_operation_type (operation_type),
INDEX idx_operation_time (operation_time DESC),
FOREIGN KEY (session_id) REFERENCES session_base_info(session_id) ON DELETE CASCADE
) COMMENT '会话操作日志表,记录所有会话相关操作';
会话扩展信息表 (session_ext_info):
CREATE TABLE session_ext_info (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
session_id VARCHAR(64) NOT NULL COMMENT '会话ID,引用session_base_info',
key VARCHAR(100) NOT NULL COMMENT '扩展键名',
value TEXT COMMENT '扩展值',
value_type TINYINT DEFAULT 1 COMMENT '值类型:1-字符串,2-数字,3-布尔值,4-JSON',
description VARCHAR(200) COMMENT '描述说明',
created_at BIGINT NOT NULL COMMENT '创建时间',
updated_at BIGINT NOT NULL COMMENT '更新时间',
UNIQUE KEY uk_session_key (session_id, key),
INDEX idx_session_id (session_id),
INDEX idx_key (key),
FOREIGN KEY (session_id) REFERENCES session_base_info(session_id) ON DELETE CASCADE
) COMMENT '会话扩展信息表,Key-Value形式存储灵活扩展信息';
表关系说明:
session_base_info是核心表,存储会话基本元数据session_private_ext和session_group_ext分别扩展单聊和群聊特有信息session_members管理用户与会话的关联关系及个人偏好session_operation_logs记录所有操作历史,便于审计和追溯session_ext_info提供灵活的扩展能力,支持未来业务需求
注:以上表结构为参考实现,实际应用中可根据业务需求调整字段名称和类型。
2.2.2.2 六表设计对会话管理功能的支持评估
本六表设计方案能够很好地管理会话管理的八大核心功能,具体支持情况如下:
1. 会话查询 - ✅ 完全支持
- 表支持:
session_base_info提供核心元数据,session_members提供用户关联关系 - 实现方式:
- 通过
session_members.user_id快速查询用户参与的会话 - 利用
session_base_info上的索引(last_message_time,updated_at)支持高效排序和分页 session_private_ext和session_group_ext提供特定会话类型的扩展信息
- 通过
2. 会话创建 - ✅ 完全支持
- 表支持:所有六表协同工作
- 实现方式:
- 服务端收到第一条消息时,自动在
session_base_info创建记录 - 根据会话类型,在
session_private_ext或session_group_ext添加扩展信息 - 在
session_members为所有参与者创建成员记录 - 通过
session_operation_logs记录创建操作
- 服务端收到第一条消息时,自动在
3. 会话更新 - ✅ 完全支持
- 表支持:
session_base_info+session_members+session_operation_logs - 实现方式:
- 收到新消息:更新
session_base_info.last_message_*字段 - 未读数变更:更新
session_members.unread_count - 用户偏好变更(置顶/静音):更新
session_members.is_top/is_muted - 所有操作都通过
session_operation_logs审计追踪
- 收到新消息:更新
4. 会话删除 - ✅ 完全支持
- 表支持:
session_members支持软删除 - 实现方式:
- 用户侧软删除:设置
session_members.is_deleted = TRUE - 级联删除:通过
ON DELETE CASCADE确保数据一致性 - 操作日志:
session_operation_logs记录删除操作
- 用户侧软删除:设置
5. 会话排序 - ✅ 完全支持
- 表支持:
session_base_info提供排序依据 - 实现方式:
- 置顶优先:
session_members.is_top决定优先级 - 时间排序:
session_base_info.last_message_timeDESC 索引 - 活跃度排序:
session_base_info.updated_at索引
- 置顶优先:
6. 未读数计算 - ✅ 完全支持
- 表支持:
session_members.unread_count专为未读设计 - 实现方式:
- 用户级未读:每个用户在
session_members有独立的unread_count - 多设备同步:服务端统一计算并推送更新
- 离线支持:客户端缓存未读数,网络恢复后同步
- 用户级未读:每个用户在
7. 离线同步 - ✅ 完全支持
- 表支持:
session_base_info.version提供乐观锁 - 实现方式:
- 版本控制:
version字段用于检测数据冲突 - 增量同步:客户端根据
sync_time请求增量更新 - 冲突解决:服务端作为权威数据源解决冲突
- 版本控制:
8. 多设备同步 - ✅ 完全支持
- 表支持:
session_members存储用户多端状态 - 实现方式:
- 状态分离:
session_members存储每个用户的偏好和状态 - 实时推送:服务端通过 WebSocket 推送状态变更
- 数据一致:服务端六表确保所有设备看到一致数据
- 状态分离:
六表设计的核心优势
| 表名 | 核心职责 | 对会话管理的贡献 |
|---|---|---|
session_base_info | 存储会话核心元数据 | 提供会话标识、最后消息、时间戳等基础信息 |
session_private_ext | 单聊特有信息 | 明确单聊参与者关系,支持快速查询 |
session_group_ext | 群聊特有信息 | 存储群组元数据(名称、头像、公告等) |
session_members | 用户与会话关联 | 最关键的表,管理未读数、置顶、静音等用户级状态 |
session_operation_logs | 操作审计 | 记录所有操作历史,支持回滚和追踪 |
session_ext_info | 灵活扩展 | 支持未来业务需求,无需修改表结构 |
设计亮点
- 职责分离:每个表专注于单一职责,避免数据冗余
- 用户级状态:
session_members表让每个用户的偏好独立管理 - 扩展性:
session_ext_info为未来功能预留空间 - 审计追踪:
session_operation_logs提供完整操作历史 - 数据一致性:外键约束和级联删除确保数据完整性
小结
这个六表设计方案完全能够胜任会话管理的所有核心功能。它通过:
- 基础表(
session_base_info)提供核心元数据 - 扩展表(
session_private_ext/session_group_ext)处理不同类型会话 - 成员表(
session_members)管理用户级状态和偏好 - 日志表(
session_operation_logs)保证可审计性 - 扩展表(
session_ext_info)确保未来可扩展
实现了会话管理的查询、创建、更新、删除、排序、未读计算、离线同步、多设备同步八大核心功能。这是一个专业、完整、可扩展的会话管理数据模型设计。
2.2.3 特殊字段说明
2.2.3.1 会话ID生成规则
✅ 字段规则: 会话ID由服务端生成,客户端不参与ID生成,仅使用服务端返回的会话ID
为什么服务端生成会话ID,而不采用客户端生成?
- 跨端一致性: 服务端作为权威数据源,确保多个客户端(PC、手机、Web等)对同一会话使用相同的ID,避免多端会话重复
- 避免冲突: 多端同时创建会话时,服务端统一生成ID避免ID冲突和数据不一致
- 数据权威: 会话元数据由服务端管理,会话ID也应由服务端生成,保持数据管理的一致性
- 易于管理: 服务端集中管理会话ID,便于后续的会话查询和管理
- 业界实践: 多数企业级IM系统采用服务端主导的会话ID生成策略
生成ID核心原则:
- 所有会话ID均由服务端生成,客户端不参与任何ID生成逻辑
- 单聊会话ID使用两个用户ID拼接(用_间隔),并按用户ID从小到大排序确保唯一性
- 群聊会话ID使用UUID格式,不添加任何前缀
- 服务端创建会话后返回完整的会话信息(包含会话ID)给客户端
会话ID命名规范:
单聊会话ID: {user_id1}_{user_id2}
- user_id1: 较小的用户ID
- user_id2: 较大的用户ID
- 使用下划线(_)作为分隔符
- 示例: 10001_10002(用户10001和用户10002的会话)
群聊会话ID: {uuid}
- 使用36位UUID v4格式(xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
- 不添加任何前缀
- 示例: a1b2c3d4-e5f6-4789-0123-456789abcdef
优点:
- 跨端一致性: 单聊ID通过用户ID拼接确保多端一致,群聊UUID保证全局唯一
- 避免重复: 服务端统一生成,避免多端各自生成导致的会话重复问题
- 易于理解: 单聊ID直接反映参与者信息,群聊UUID保持简洁
- 便于调试: 单聊会话可以快速识别参与者,群聊会话UUID便于追踪
- 高效查询: 单聊ID包含用户信息,支持基于用户ID的快速查询
- 扩展性好: 未来可以轻松添加其他会话类型,只需定义相应的ID生成规则
三、会话维度组合
3.1 会话管理方案的关键维度
会话管理方案的设计涉及多个关键维度,每个维度的选择都会影响最终的技术实现和用户体验。以下是影响方案选择的核心维度:
维度一:会话创建时机:
- 显式创建:用户点击联系人时,先发送WS消息创建会话
- 隐式创建:直接发送消息,服务端自动创建会话
维度二:数据存储策略:
- 服务端存储为主:会话数据主要存储在服务端
- 客户端存储为主:会话数据主要存储在客户端
- 混合存储:两端都存储,通过同步机制保持一致性
维度三:状态管理方式:
- 服务端计算状态:未读数、最后消息等由服务端计算
- 客户端计算状态:状态由客户端本地计算
- 混合计算:关键状态由服务端计算,辅助状态由客户端计算
维度四:同步机制:
- 实时同步:通过WebSocket实时同步
- 定时同步:定时轮询同步
- 按需同步:根据用户操作按需同步
维度五:数据一致性策略:
- 强一致性:所有设备数据完全一致,实时同步
- 最终一致性:允许短暂不一致,最终达到一致
- 弱一致性:不保证数据一致性,以用户体验优先
维度六:离线处理策略:
- 离线优先:离线时功能完整,同步时合并数据
- 在线优先:离线时功能受限,依赖网络恢复
- 渐进式同步:重要数据优先同步,其他数据按需同步
3.2 维度分析
3.2.1 会话创建时机
会话创建时机是指会话记录在系统中产生的时刻,分为隐式创建或显式创建,是会话管理方案的核心维度之一。
1. 隐式创建
定义:当用户发送第一条消息或接收第一条消息时,服务端自动创建会话记录,无需客户端显式请求创建会话。
核心思想:会话的存在是消息交互的自然结果,而非用户的显式操作。
优点:
- ✅ 用户体验流畅,无需等待会话创建
- ✅ 减少网络请求次数,提高性能
- ✅ 逻辑简单,降低实现复杂度
- ✅ 符合用户心智模型(发送消息=开始聊天)
缺点:
- ❌ 会话列表可能有延迟(第一条消息发送后才出现)
- ❌ 需要服务端处理并发创建的竞争条件
- ❌ 无法预知会话是否存在(需要首次交互)
适用场景:
- 即时通讯IM等主流IM应用
- 强调即时通信体验的应用
- 用户频繁发送新消息的场景
相关实现:
📋 点击展开/收起 前端部分
// 伪代码:仅用于展示核心逻辑,非完整实现
/**
* 发送消息 - 隐式创建会话
* 客户端直接发送消息,无需先创建会话
*/
async function sendMessage(toId: number, content: string, scene: Scene) {
// 直接发送消息,无需检查会话是否存在
const message = {
toId,
content,
scene,
type: MessageType.Text,
timestamp: Date.now()
};
try {
// 通过WebSocket发送消息到服务端
await ws.send(JSON.stringify({
type: 'message',
data: message
}));
// 服务端会自动创建会话(如果不存在)并返回消息ID
console.log('消息发送成功');
} catch (error) {
console.error('消息发送失败:', error);
throw new Error('消息发送失败');
}
}
// 类型定义(伪代码)
enum Scene {
PrivateChat = 1,
GroupChat = 2
}
enum MessageType {
Text = 1,
Image = 2,
Voice = 3,
Video = 4
}
// WebSocket连接管理(伪代码)
const ws = new WebSocket('wss://your-im-server.com/ws');
ws.onopen = () => console.log('WebSocket连接已建立');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// 处理服务端返回的消息
if (data.type === 'message_ack') {
console.log('消息已确认:', data.messageId);
} else if (data.type === 'new_session') {
console.log('新会话创建:', data.session);
// 更新会话列表
}
};
📋 点击展开/收起 后端部分
// 伪代码:仅用于展示核心逻辑,非完整实现
/**
* 消息处理服务 - 隐式创建会话
*/
@Injectable()
export class MessageService {
constructor(
private sessionRepository: SessionRepository,
private privateSessionExtRepository: PrivateSessionExtRepository,
private groupSessionExtRepository: GroupSessionExtRepository,
private sessionMembersRepository: SessionMembersRepository,
private sessionOperationLogsRepository: SessionOperationLogsRepository,
private websocketService: WebsocketService
) {}
async handleMessage(message: MessageDTO) {
try {
// 1. 检查会话是否存在
let sessionId = await this.getSessionId(message.toId, message.scene);
// 2. 如果会话不存在,自动创建
if (!sessionId) {
sessionId = await this.createSession(message);
}
// 3. 保存消息
const savedMessage = await this.saveMessage({
...message,
sessionId
});
// 4. 更新会话的最后消息
await this.updateSessionLastMessage(sessionId, savedMessage);
// 5. 推送消息给接收方
await this.pushMessage(savedMessage);
return sessionId;
} catch (error) {
console.error('处理消息失败:', error);
throw error;
}
}
/**
* 获取会话ID
*/
private async getSessionId(toId: number, scene: Scene): Promise<string | null> {
// 实现逻辑:根据接收者ID和场景查询会话
// 伪代码实现
if (scene === Scene.PrivateChat) {
const currentUserId = this.getCurrentUserId();
const userIds = [currentUserId, toId].sort((a, b) => a - b);
return `${userIds[0]}_${userIds[1]}`;
} else {
// 群聊场景:返回群ID作为会话ID(群ID即为会话ID)
// 注意:隐式创建模式下,群聊会话的toId即为群组ID,该ID在创建群组时已生成并作为会话ID
return toId.toString();
}
}
/**
* 保存消息
*/
private async saveMessage(message: any) {
// 实现逻辑:保存消息到数据库
// 伪代码实现
return message;
}
/**
* 更新会话最后消息
*/
private async updateSessionLastMessage(sessionId: string, message: any) {
// 实现逻辑:更新会话的最后消息信息
// 伪代码实现
await this.sessionRepository.update({
sessionId,
lastMessageId: message.id,
lastMessageContent: message.content,
lastMessageType: message.type,
lastMessageTime: message.timestamp,
updatedAt: Date.now()
});
}
/**
* 推送消息
*/
private async pushMessage(message: any) {
// 实现逻辑:通过WebSocket推送消息给接收方
// 伪代码实现
await this.websocketService.sendMessageToUser(message.toId, {
type: 'new_message',
data: message
});
}
/**
* 自动创建会话
*/
private async createSession(message: MessageDTO): Promise<string> {
// 1. 生成会话ID
const sessionId = this.generateSessionId(message.toId, message.scene);
// 2. 检查会话类型,判断是单聊还是群聊
const isGroupChat = message.scene === Scene.GroupChat;
if (isGroupChat) {
// 群聊:toId即为群组ID(群聊会话的toId已在创建群组时生成,此处直接使用)
// 注:群聊的会话ID就是群组ID,不需要重新生成UUID
await this.createGroupSession(message, sessionId);
} else {
// 单聊:创建单聊会话
await this.createPrivateSession(message, sessionId);
}
// 3. 推送新会话给发送方
await this.pushNewSession(sessionId, message.fromId);
// 4. 推送新会话给接收方
await this.pushNewSession(sessionId, message.toId);
return sessionId;
}
/**
* 生成会话ID
*/
private generateSessionId(toId: number, scene: Scene): string {
if (scene === Scene.PrivateChat) {
const currentUserId = this.getCurrentUserId();
const userIds = [currentUserId, toId].sort((a, b) => a - b);
return `${userIds[0]}_${userIds[1]}`;
} else {
// 群聊:toId即为群组ID(群聊会话的toId在创建群组时已生成,此处直接使用)
// 注:群聊的会话ID就是群组ID,不需要重新生成UUID
return toId.toString();
}
}
/**
* 生成UUID
*/
private generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* 获取当前用户ID
*/
private getCurrentUserId(): number {
// 实现逻辑:从上下文获取当前用户ID
// 伪代码实现
return 10001;
}
/**
* 推送新会话
*/
private async pushNewSession(sessionId: string, userId: number) {
// 实现逻辑:推送新会话给用户
// 伪代码实现
const session = await this.sessionRepository.getSessionById(sessionId);
await this.websocketService.sendMessageToUser(userId, {
type: 'new_session',
data: session
});
}
/**
* 创建单聊会话(隐式创建)
*/
private async createPrivateSession(message: MessageDTO, sessionId: string): Promise<void> {
const userIds = [message.fromId, message.toId].sort((a, b) => a - b);
// 1. 保存会话基本信息到session_base_info表
await this.sessionRepository.save({
sessionId,
sessionType: SessionType.Private,
lastMessageId: '',
lastMessageContent: message.content,
lastMessageType: message.type,
lastMessageTime: message.timestamp,
createdAt: Date.now(),
updatedAt: Date.now(),
version: 0
});
// 2. 保存单聊扩展信息到session_private_ext表
await this.privateSessionExtRepository.save({
sessionId,
userId1: userIds[0],
userId2: userIds[1],
createdAt: Date.now(),
updatedAt: Date.now()
});
// 3. 创建会话成员到session_members表(两个用户)
await this.sessionMembersRepository.createMember({
sessionId,
userId: userIds[0],
role: MemberRole.Member,
unreadCount: 0,
isTop: false,
isMuted: false,
isDeleted: false,
joinTime: Date.now(),
isActive: true,
createdAt: Date.now(),
updatedAt: Date.now()
});
await this.sessionMembersRepository.createMember({
sessionId,
userId: userIds[1],
role: MemberRole.Member,
unreadCount: 0,
isTop: false,
isMuted: false,
isDeleted: false,
joinTime: Date.now(),
isActive: true,
createdAt: Date.now(),
updatedAt: Date.now()
});
// 4. 记录操作日志
await this.sessionOperationLogsRepository.save({
sessionId,
operatorId: message.fromId,
operationType: 7, // 7-自动创建
operationTime: Date.now(),
createdAt: Date.now()
});
}
/**
* 创建群聊会话(隐式创建)
*/
private async createGroupSession(message: MessageDTO, sessionId: string): Promise<void> {
// 1. 保存会话基本信息到session_base_info表
await this.sessionRepository.save({
sessionId,
sessionType: SessionType.Group,
lastMessageId: '',
lastMessageContent: message.content,
lastMessageType: message.type,
lastMessageTime: message.timestamp,
createdAt: Date.now(),
updatedAt: Date.now(),
version: 0
});
// 2. 保存群聊扩展信息到session_group_ext表
await this.groupSessionExtRepository.save({
sessionId,
groupName: message.groupName || '群聊',
groupAvatar: message.groupAvatar || '',
announcement: '',
maxMembers: 500,
joinPermission: 1,
messagePermission: 1,
createdAt: Date.now(),
updatedAt: Date.now()
});
// 3. 创建会话成员到session_members表(群聊成员)
// 注:隐式创建模式下,群聊成员应从群组管理中获取,不包含在message中
// 这里假设members字段已从群组管理中获取
const members = message.members || [message.fromId];
for (const memberId of members) {
await this.sessionMembersRepository.createMember({
sessionId,
userId: memberId,
role: memberId === message.fromId ? MemberRole.Owner : MemberRole.Member,
unreadCount: 0,
isTop: false,
isMuted: false,
isDeleted: false,
joinTime: Date.now(),
isActive: true,
createdAt: Date.now(),
updatedAt: Date.now()
});
}
// 4. 记录操作日志
await this.sessionOperationLogsRepository.save({
sessionId,
operatorId: message.fromId,
operationType: 7, // 7-自动创建
operationTime: Date.now(),
createdAt: Date.now()
});
}
}
// 类型定义(伪代码)
enum Scene {
PrivateChat = 1,
GroupChat = 2
}
enum SessionType {
Private = 1,
Group = 2,
System = 3
}
enum MemberRole {
Member = 1,
Admin = 2,
Owner = 3
}
// DTO定义(伪代码)
class MessageDTO {
fromId: number;
toId: number;
content: string;
type: number;
scene: Scene;
timestamp: number;
groupName?: string;
groupAvatar?: string;
members?: number[];
}
2. 显式创建
定义:用户需要先通过特定操作(如点击联系人、新建会话按钮)创建会话,创建成功后才能发送消息。
核心思想:会话是独立于消息存在的实体,用户可以主动管理会话。
优点:
- ✅ 会话列表立即可见(无需等待第一条消息)
- ✅ 可以预先创建会话,便于管理
- ✅ 支持会话模板、预设信息等高级功能
- ✅ 更好的会话生命周期管理
缺点:
- ❌ 用户体验稍差,需要等待会话创建
- ❌ 增加网络请求,影响性能
- ❌ 逻辑复杂度较高
- ❌ 不符合用户即时通讯的心智模型
适用场景:
- 需要预设会话信息的场景
- 强调会话管理的应用
实现流程:
📋 点击展开/收起 前端部分
// 伪代码:仅用于展示核心逻辑,非完整实现
/**
* 创建会话 - 显式创建
* 用户需要先创建会话,然后才能发送消息
*/
async function createSession(toId: number, scene: Scene) {
try {
// 1. 先检查会话是否已存在
const existingSession = await checkSessionExist(toId, scene);
if (existingSession) {
console.log('会话已存在');
return existingSession.sessionId;
}
// 2. 请求服务端创建会话
const response = await fetch('/api/sessions', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ toId, scene })
});
if (!response.ok) {
throw new Error('创建会话失败');
}
const session = await response.json();
// 3. 会话创建成功后,才能发送消息
if (session.success) {
console.log('会话创建成功,可以发送消息');
return session.sessionId;
} else {
throw new Error(session.message || '创建会话失败');
}
} catch (error) {
console.error('创建会话失败:', error);
throw error;
}
}
/**
* 检查会话是否存在
*/
async function checkSessionExist(toId: number, scene: Scene): Promise<any> {
try {
const response = await fetch(`/api/sessions/check?toId=${toId}&scene=${scene}`);
const result = await response.json();
return result.exists ? result.session : null;
} catch (error) {
console.error('检查会话失败:', error);
return null;
}
}
/**
* 发送消息 - 显式创建模式
* 需要先确保会话存在
*/
async function sendMessage(sessionId: string, content: string) {
// 检查会话是否存在
if (!sessionId) {
throw new Error('会话不存在,请先创建会话');
}
try {
// 发送消息
const message = {
sessionId,
content,
type: MessageType.Text,
timestamp: Date.now()
};
await ws.send(JSON.stringify({
type: 'message',
data: message
}));
console.log('消息发送成功');
} catch (error) {
console.error('消息发送失败:', error);
throw error;
}
}
// 类型定义(伪代码)
enum Scene {
PrivateChat = 1,
GroupChat = 2
}
enum MessageType {
Text = 1,
Image = 2,
Voice = 3,
Video = 4
}
📋 点击展开/收起 后端部分
// 伪代码:仅用于展示核心逻辑,非完整实现
/**
* 会话管理服务 - 显式创建会话
*/
@Injectable()
export class SessionService {
constructor(
private sessionRepository: SessionRepository,
private privateSessionExtRepository: PrivateSessionExtRepository,
private groupSessionExtRepository: GroupSessionExtRepository,
private sessionMembersRepository: SessionMembersRepository,
private sessionOperationLogsRepository: SessionOperationLogsRepository,
private websocketService: WebsocketService
) {}
/**
* 创建会话(显式创建)
*/
async createSession(dto: CreateSessionDTO): Promise<Session> {
try {
const currentUserId = this.getCurrentUserId();
const { toId } = dto;
// 1. 判断会话类型并生成会话ID
const isGroupChat = dto.isGroupChat || false;
let sessionId: string;
if (isGroupChat) {
// 群聊:生成UUID
sessionId = this.generateUUID();
} else {
// 单聊:生成会话ID(小用户ID_大用户ID)
const userIds = [currentUserId, toId].sort((a, b) => a - b);
sessionId = `${userIds[0]}_${userIds[1]}`;
}
// 2. 检查会话是否已存在
const existing = await this.sessionRepository.getSessionById(sessionId);
if (existing) {
throw new BadRequestException('会话已存在');
}
// 3. 创建会话基本信息
const sessionType = isGroupChat ? SessionType.Group : SessionType.Private;
await this.createBaseSession({
sessionId,
sessionType
});
// 4. 根据会话类型保存扩展信息
if (isGroupChat) {
// 群聊扩展信息
await this.groupSessionExtRepository.save({
sessionId,
groupName: dto.groupName || '群聊',
groupAvatar: dto.groupAvatar || '',
announcement: '',
maxMembers: 500,
joinPermission: 1,
messagePermission: 1,
createdAt: Date.now(),
updatedAt: Date.now()
});
} else {
// 单聊扩展信息
const userIds = [currentUserId, toId].sort((a, b) => a - b);
await this.privateSessionExtRepository.save({
sessionId,
userId1: userIds[0],
userId2: userIds[1],
createdAt: Date.now(),
updatedAt: Date.now()
});
}
// 5. 创建会话成员
if (isGroupChat) {
// 群聊:创建群主和成员
const members = dto.memberIds || [];
await this.createMember({
sessionId,
userId: currentUserId,
role: MemberRole.Owner
});
for (const memberId of members) {
await this.createMember({
sessionId,
userId: memberId,
role: MemberRole.Member
});
}
} else {
// 单聊:创建两个成员
const userIds = [currentUserId, toId].sort((a, b) => a - b);
await this.createMember({
sessionId,
userId: userIds[0],
role: MemberRole.Member
});
await this.createMember({
sessionId,
userId: userIds[1],
role: MemberRole.Member
});
}
// 6. 记录操作日志
await this.sessionOperationLogsRepository.save({
sessionId,
operatorId: currentUserId,
operationType: 7, // 7-创建会话
operationTime: Date.now(),
createdAt: Date.now()
});
// 7. 获取完整的会话信息
const session = await this.sessionRepository.getSessionById(sessionId);
// 8. 推送新会话给所有参与者
const memberIds = isGroupChat
? [currentUserId, ...(dto.memberIds || [])]
: [currentUserId, toId];
await this.pushNewSession(session, memberIds);
return session;
} catch (error) {
console.error('创建会话失败:', error);
throw error;
}
}
/**
* 获取当前用户ID
*/
private getCurrentUserId(): number {
// 实现逻辑:从上下文获取当前用户ID
// 伪代码实现
return 10001;
}
/**
* 推送新会话
*/
private async pushNewSession(session: Session, memberIds: number[]) {
// 实现逻辑:推送新会话给所有成员
// 伪代码实现
for (const memberId of memberIds) {
await this.websocketService.sendMessageToUser(memberId, {
type: 'new_session',
data: session
});
}
}
/**
* 创建会话基本信息
*/
private async createBaseSession(dto: {
sessionId: string,
sessionType: SessionType
}): Promise<void> {
await this.sessionRepository.save({
sessionId,
sessionType,
lastMessageId: '',
lastMessageContent: '',
lastMessageType: null,
lastMessageTime: 0,
createdAt: Date.now(),
updatedAt: Date.now(),
version: 0
});
}
/**
* 创建会话成员
*/
private async createMember(dto: {
sessionId: string,
userId: number,
role: MemberRole
}): Promise<void> {
await this.sessionMembersRepository.create({
sessionId,
userId,
role,
unreadCount: 0,
isTop: false,
isMuted: false,
isDeleted: false,
joinTime: Date.now(),
isActive: true,
createdAt: Date.now(),
updatedAt: Date.now()
});
}
/**
* 生成UUID(用于群聊会话ID)
*/
private generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
}
// 类型定义(伪代码)
enum SessionType {
Private = 1,
Group = 2,
System = 3
}
enum MemberRole {
Member = 1,
Admin = 2,
Owner = 3
}
// DTO定义(伪代码)
class CreateSessionDTO {
toId: number;
isGroupChat?: boolean;
groupName?: string;
groupAvatar?: string;
memberIds?: number[];
}
class Session {
sessionId: string;
sessionType: SessionType;
lastMessageId: string;
lastMessageContent: string;
lastMessageType: number | null;
lastMessageTime: number;
createdAt: number;
updatedAt: number;
version: number;
}
// 异常类(伪代码)
class BadRequestException extends Error {
constructor(message: string) {
super(message);
this.name = 'BadRequestException';
}
}
两种创建时机的对比
| 对比维度 | 隐式创建 | 显式创建 |
|---|---|---|
| 用户体验 | 流畅自然,无等待感 | 需要等待会话创建 |
| 网络请求 | 减少一次请求 | 增加一次创建会话请求 |
| 实现复杂度 | 较简单 | 较复杂 |
| 会话列表 | 第一条消息后才显示 | 创建后立即可见 |
| 适用场景 | 即时通讯、社交应用 | 企业协作、群组管理 |
| 主流度 | 主流方案 | 特定场景使用 |
| 心智模型 | 发送消息=开始聊天 | 先建会话=再发消息 |
小结
两种方式都可以,根据实际场景选择。
3.2.2 数据存储
数据存储策略决定了会话数据在服务端和客户端之间的分布和同步方式,是影响系统性能、一致性和用户体验的核心维度。
数据存储策略分类:
- 服务端存储为主(企业级推荐)
- 客户端存储为主(轻量级方案)
- 混合存储(平衡方案)
1. 服务端存储为主
定义:会话数据主要存储在服务端数据库,客户端仅作为视图层,通过API获取会话列表和状态。
核心思想:服务端作为数据权威来源,保证多端数据一致性。
架构特点:
- 会话表完全存储在服务端数据库(MySQL/PostgreSQL等)
- 客户端缓存少量数据用于离线访问
- 所有会话操作(创建、更新、删除)都通过服务端API
- 服务端实时推送会话变更给所有客户端
数据流设计:
graph TD
A[用户操作] --> B{操作类型}
B -->|查询| C[HTTP请求会话列表]
B -->|更新| D[WebSocket推送操作]
B -->|离线| E[读取本地缓存]
C --> F[服务端数据库查询]
D --> G[服务端数据更新]
G --> H[WebSocket推送到所有客户端]
F --> I[返回会话数据]
I --> J[客户端缓存更新]
H --> J
E --> K[展示UI]
J --> K
优点:
- ✅ 数据一致性高,多端自动同步
- ✅ 服务端作为权威数据源,数据准确可靠
- ✅ 客户端轻量级,实现简单
- ✅ 安全性高,敏感数据(未读数等)由服务端控制
- ✅ 支持复杂的数据查询和统计分析
- ✅ 易于备份和恢复
缺点:
- ❌ 服务端负载较重,需要高性能数据库
- ❌ 网络依赖性强,离线体验受限
- ❌ 需要设计离线缓存策略
- ❌ 实时性依赖网络状况
适用场景:
- 企业级IM应用
- 需要强数据一致性的应用
- 多设备同步要求高的应用
- 需要数据统计分析的应用
2. 客户端存储为主
定义:会话数据主要存储在客户端本地数据库,服务端仅用于消息传输和部分数据同步。
核心思想:客户端作为数据主人,服务端仅提供消息传输能力。
架构特点:
- 会话数据存储在客户端本地数据库(SQLite、IndexedDB等)
- 服务端仅存储消息和部分会话元数据
- 客户端本地计算会话状态(未读数、排序等)
- 服务端仅同步必要的会话变更
优点:
- ✅ 离线体验极佳,本地数据随时可用
- ✅ 响应速度快,无需等待网络请求
- ✅ 服务端负载轻,节省服务器资源
- ✅ 实现简单,客户端直接管理数据
- ✅ 隐私性好,数据本地化
缺点:
- ❌ 多端数据一致性差
- ❌ 需要设计复杂的同步策略
- ❌ 数据冲突处理复杂
- ❌ 不支持跨设备数据查询
- ❌ 数据备份和恢复困难
适用场景:
- 单设备应用(如桌面端IM)
- 对离线体验要求极高的应用
- 隐私敏感型应用
- 轻量级IM应用
3. 混合存储
定义:会话数据在服务端和客户端都存储,重要数据由服务端统一管理,辅助数据由客户端本地管理。
核心思想:平衡服务端和客户端的优势,实现最佳用户体验。
数据分类:
| 数据类型 | 存储位置 | 同步策略 |
|---|---|---|
| 会话ID | 服务端 | 必须同步 |
| 会话类型 | 服务端 | 必须同步 |
| 最后消息 | 服务端 | 实时同步 |
| 未读数 | 服务端 | 实时同步 |
| 置顶状态 | 服务端 | 必须同步 |
| 静音状态 | 服务端 | 必须同步 |
| 删除状态 | 服务端 | 必须同步 |
| UI展开状态 | 客户端 | 本地存储 |
| 滚动位置 | 客户端 | 本地存储 |
| 排序偏好 | 客户端 | 本地存储 |
优点:
- ✅ 平衡服务端和客户端优势
- ✅ 重要数据保证一致性
- ✅ UI状态响应速度快
- ✅ 离线体验良好
- ✅ 灵活性强,可定制
缺点:
- ❌ 实现复杂度最高
- ❌ 需要仔细划分数据归属
- ❌ 维护成本高
- ❌ 需要设计冲突解决策略
适用场景:
- 需要平衡各种需求的应用
- 业务复杂度高的应用
- 需要良好离线体验的应用
三种存储策略对比
| 对比维度 | 服务端存储为主 | 客户端存储为主 | 混合存储 |
|---|---|---|---|
| 数据一致性 | 最高(服务端统一管理) | 低(依赖同步) | 高(重要数据服务端) |
| 离线体验 | 差(依赖服务端缓存) | 最好(本地完整) | 良好(关键数据缓存) |
| 响应速度 | 慢(需要网络请求) | 快(本地直接访问) | 较快(重要数据缓存) |
| 服务端负载 | 重(需要高性能数据库) | 轻(仅同步) | 中(管理重要数据) |
| 实现复杂度 | 中 | 低 | 高 |
| 多端同步 | 最好(服务端推送) | 差(需要复杂同步) | 好(关键数据同步) |
| 数据安全 | 高(服务端备份) | 低(本地存储风险) | 高(重要数据服务端) |
| 适用场景 | 企业级IM、多设备同步 | 单设备、离线优先 | 需要平衡的应用 |
| 主流度 | 主流方案 | 特定场景 | 渐趋流行 |
3.2.3 状态管理方式
定义:状态管理方式指会话状态(如未读数、最后消息、置顶状态、静音状态等)由谁计算、维护和同步。
分类:
- 服务端计算状态:所有关键状态由服务端统一计算和维护,客户端仅作为展示层
- 客户端计算状态:状态由客户端本地计算,服务端仅提供原始数据
- 混合计算:关键状态由服务端计算,UI相关状态由客户端管理(方案三采用)
混合计算的核心思想:
- 服务端计算关键状态:未读数、最后消息、会话元数据等保证多端一致的状态由服务端计算
- 客户端管理UI状态:界面展开状态、滚动位置、排序偏好等仅影响当前设备的状态由客户端管理
- 实时同步关键状态:服务端计算的关键状态通过WebSocket实时推送给所有客户端
- 本地持久化UI状态:UI状态保存在客户端本地数据库,无需同步
优点:
- ✅ 数据一致性:关键状态由服务端统一计算,保证多端一致
- ✅ 响应速度:UI状态本地管理,界面响应迅速
- ✅ 网络优化:减少不必要的状态同步流量
- ✅ 用户体验:离线时UI状态保持,上线后关键状态自动同步
缺点:
- ❌ 实现复杂度:需要清晰划分状态归属,增加设计复杂度
- ❌ 冲突处理:需要设计状态冲突解决策略
- ❌ 维护成本:两端都需要维护状态管理逻辑
适用场景:
- 需要多端数据一致性的企业级IM应用
- 对离线体验有要求的应用
- 需要快速UI响应的复杂应用
3.2.4 同步机制
定义:同步机制指会话数据在服务端和客户端之间保持一致的更新和传输方式。
分类:
- 实时同步:通过WebSocket等长连接实时推送数据变更
- 定时同步:定期轮询服务端获取最新数据
- 按需同步:根据用户操作或应用状态按需请求数据
- 混合同步:结合多种同步方式(方案三采用)
混合同步的核心思想:
- 实时推送关键变更:新消息、未读数变化、会话状态更新等通过WebSocket实时推送
- 按需拉取完整数据:会话列表、历史消息等大数据量内容按需请求
- 定时同步离线状态:网络恢复后定时同步离线期间的数据变更
- 智能降级策略:网络差时自动降级为按需同步,保证基本功能
同步策略:
| 数据类型 | 同步机制 | 触发条件 | 说明 |
|---|---|---|---|
| 新消息 | 实时推送 | 消息到达 | WebSocket即时推送 |
| 未读数 | 实时推送 | 未读变化 | 服务端计算后推送 |
| 会话状态 | 实时推送 | 状态变更 | 置顶、静音等变更 |
| 会话列表 | 按需拉取 | 应用启动/刷新 | HTTP请求完整列表 |
| 离线操作 | 定时同步 | 网络恢复 | 同步离线期间操作 |
| UI状态 | 本地存储 | 无 | 仅本地持久化 |
优点:
- ✅ 实时性高:关键变更实时推送,用户体验好
- ✅ 网络优化:大数据量按需拉取,减少不必要流量
- ✅ 离线支持:离线操作入队,网络恢复后自动同步
- ✅ 智能适应:根据网络状况自动调整同步策略
缺点:
- ❌ 实现复杂:需要维护多种同步机制
- ❌ 服务端压力:实时推送需要维护大量WebSocket连接
- ❌ 客户端复杂度:需要处理多种同步场景
适用场景:
- 对实时性要求高的IM应用
- 需要良好离线体验的应用
- 网络环境复杂的多设备应用
3.2.5 数据一致性策略
定义:数据一致性策略指在多设备、多用户场景下,如何保证会话数据的一致性。
分类:
- 强一致性:所有设备数据完全一致,实时同步(性能开销大)
- 最终一致性:允许短暂不一致,通过同步机制保证最终一致(方案三采用)
- 弱一致性:不保证数据一致性,以用户体验优先
最终一致性的核心思想:
- 允许短暂不一致:多设备间允许短暂的数据不一致,优先保证用户体验
- 自动冲突解决:服务端作为权威数据源,自动解决数据冲突
- 增量同步:只同步变更的数据,减少网络传输
- 乐观锁机制:通过版本号检测冲突,保证数据最终一致
一致性保证级别:
| 数据类型 | 一致性级别 | 同步延迟 | 冲突解决 |
|---|---|---|---|
| 会话ID | 强一致性 | 实时 | 服务端生成,无冲突 |
| 会话类型 | 强一致性 | 实时 | 服务端定义,无冲突 |
| 最后消息 | 最终一致性 | <1s | 服务端时间戳优先 |
| 未读数 | 最终一致性 | <1s | 服务端计算为准 |
| 置顶状态 | 最终一致性 | <1s | 最新操作为准 |
| 静音状态 | 最终一致性 | <1s | 最新操作为准 |
| 删除状态 | 最终一致性 | <1s | 最新操作为准 |
| UI状态 | 弱一致性 | 无同步 | 仅本地有效 |
优点:
- ✅ 性能优异:相比强一致性,显著降低服务端压力和网络开销
- ✅ 用户体验:允许短暂不一致,界面响应迅速
- ✅ 离线友好:离线操作入队,上线后自动同步
- ✅ 扩展性强:支持大规模多设备同步
缺点:
- ❌ 短暂不一致:多设备间可能存在短暂的数据不一致
- ❌ 冲突处理:需要设计完善的冲突解决策略
- ❌ 实现复杂度:需要维护版本号和同步逻辑
适用场景:
- 多设备同步的IM应用
- 对实时性要求不是极端严格的应用
- 需要平衡性能和一致性的应用
3.2.6 离线处理策略
定义:离线处理策略指在网络断开时,如何保证会话功能的基本可用性和数据完整性。
分类:
- 离线优先:离线时功能完整,同步时合并数据(方案三采用)
- 在线优先:离线时功能受限,依赖网络恢复
- 渐进式同步:重要数据优先同步,其他数据按需同步
离线优先的核心思想:
- 本地数据库:会话数据完整缓存在客户端本地数据库
- 离线操作队列:离线时的操作(发送消息、置顶、删除等)存入待同步队列
- 自动冲突解决:网络恢复后自动同步离线操作,服务端解决冲突
- 渐进式合并:大数据量采用增量合并,减少网络负担
离线处理流程:
- 网络检测:实时检测网络状态,进入离线模式
- 本地操作:用户操作直接写入本地数据库,保证功能可用
- 操作入队:需要服务端确认的操作加入待同步队列
- 网络恢复:检测到网络恢复,启动同步流程
- 增量同步:只同步离线期间的变更数据
- 冲突解决:服务端作为权威数据源解决冲突
- 状态更新:客户端更新本地数据,刷新UI
优点:
- ✅ 离线体验:网络断开时基本功能完整可用
- ✅ 数据完整:离线操作可靠保存,网络恢复后自动同步
- ✅ 用户体验:无缝切换在线/离线状态,用户无感知
- ✅ 网络优化:只同步变更数据,减少流量消耗
缺点:
- ❌ 实现复杂度:需要设计完善的离线同步和冲突解决
- ❌ 存储开销:客户端需要维护完整的数据副本
- ❌ 同步延迟:网络恢复后需要时间完成同步
适用场景:
- 对离线体验要求高的IM应用
- 网络环境不稳定的应用
- 需要保证数据完整性的应用
3.3 业界最佳实践方案
3.3.1 方案一:传统企业级IM方案
适用场景:大型企业应用,多设备同步,高一致性要求
维度组合:
- 会话创建:隐式创建
- 数据存储:混合存储
- 状态管理:服务端计算关键状态
- 同步机制:实时同步 + 按需同步
- 数据一致性:强一致性
- 离线处理:渐进式同步
核心特点:
- 服务端作为权威数据源,保证多设备一致性
- 客户端缓存实时状态,提供快速响应
- 支持离线使用,网络恢复后自动同步
优点:
- ✅ 数据一致性高,多设备自动同步
- ✅ 用户体验流畅,无需等待会话创建
- ✅ 支持离线使用,网络恢复后自动同步
- ✅ 安全性高,服务端控制敏感数据
缺点:
- ❌ 服务端负载较重
- ❌ 网络依赖性强
- ❌ 实现复杂度较高
3.3.2 方案二:轻量级实时IM方案
适用场景:中小型应用,实时性要求高,云端存储为主
维度组合:
- 会话创建:隐式创建
- 数据存储:云端存储为主
- 状态管理:云端计算状态
- 同步机制:实时同步
- 数据一致性:云端强一致性
- 离线处理:在线优先
核心特点:
- 云端统一管理所有会话状态
- 客户端轻量级,主要负责展示
- 实时性极佳,所有状态云端维护
优点:
- ✅ 实时性极佳,所有状态实时同步
- ✅ 客户端轻量级,实现相对简单
- ✅ 多设备体验一致
- ✅ 云端数据安全可靠
缺点:
- ❌ 离线体验差
- ❌ 网络依赖性强
- ❌ 服务端压力大
3.3.3 方案三:混合弹性IM方案(自定义模式)
适用场景:需要平衡各种需求,业务复杂度高
维度组合:
- 会话创建:显式创建(本方案采用显式创建方式)
- 数据存储:混合存储,重要数据服务端,辅助数据客户端
- 状态管理:混合计算,关键状态服务端,UI状态客户端
- 同步机制:实时同步 + 定时同步 + 按需同步
- 数据一致性:最终一致性
- 离线处理:渐进式同步
核心特点:
- 灵活的配置策略,可适应不同业务场景
- 平衡各种需求,兼顾性能和一致性
- 支持复杂业务逻辑
优点:
- ✅ 灵活性强,可适应不同业务场景
- ✅ 平衡各种需求,兼顾性能和一致性
- ✅ 扩展性好,支持复杂业务逻辑
- ✅ 用户体验良好,响应迅速
缺点:
- ❌ 实现最复杂
- ❌ 维护成本高
- ❌ 需要精心设计
3.3.4 比较总结
| 比较维度 | 方案一:传统企业级IM方案 | 方案二:轻量级实时IM方案 | 方案三:混合弹性IM方案(自定义模式) |
|---|---|---|---|
| 适用场景 | 大型企业应用,多设备同步,高一致性要求 | 中小型应用,实时性要求高,云端存储为主 | 需要平衡各种需求,业务复杂度高 |
| 会话创建 | 隐式创建 | 隐式创建 | 显式创建 |
| 数据存储 | 混合存储 | 云端存储为主 | 混合存储,重要数据服务端,辅助数据客户端 |
| 状态管理 | 服务端计算关键状态 | 云端计算状态 | 混合计算,关键状态服务端,UI状态客户端 |
| 同步机制 | 实时同步 + 按需同步 | 实时同步 | 实时同步 + 定时同步 + 按需同步 |
| 数据一致性 | 强一致性 | 云端强一致性 | 最终一致性 |
| 离线处理 | 渐进式同步 | 在线优先 | 渐进式同步 |
| 优点 | ✅ 数据一致性高,多设备自动同步 ✅ 用户体验流畅 ✅ 支持离线使用 ✅ 安全性高 | ✅ 实时性极佳 ✅ 客户端轻量级 ✅ 多设备体验一致 ✅ 云端数据安全可靠 | ✅ 灵活性强,可适应不同业务场景 ✅ 平衡各种需求,兼顾性能和一致性 ✅ 扩展性好,支持复杂业务逻辑 ✅ 用户体验良好,响应迅速 |
| 缺点 | ❌ 服务端负载较重 ❌ 网络依赖性强 ❌ 实现复杂度较高 | ❌ 离线体验差 ❌ 网络依赖性强 ❌ 服务端压力大 | ❌ 实现最复杂 ❌ 维护成本高 ❌ 需要精心设计 |
四、方案三(自定义模式)的详细设计与实现
4.1 方案概述
方案三采用服务端主导的混合存储策略,平衡了数据一致性、离线体验和系统性能。
核心设计理念:
- 服务端作为权威数据源:所有会话数据由服务端统一管理和生成
- 客户端智能缓存:客户端缓存会话数据用于离线访问和快速响应
- 实时+按需同步:通过WebSocket实时推送关键变更,按需拉取完整数据
- 最终一致性:允许短暂的数据不一致,通过同步机制保证最终一致
维度组合:
| 维度 | 方案 | 说明 |
|---|---|---|
| 会话创建 | 显式创建 | 发送消息前先创建会话 |
| 数据存储 | 混合存储 | 重要数据服务端,辅助数据客户端 |
| 状态管理 | 混合计算 | 关键状态服务端,UI状态客户端 |
| 同步机制 | 实时+按需 | WebSocket推送 + HTTP拉取 |
| 数据一致性 | 最终一致性 | 允许短暂不一致,定期同步 |
| 离线处理 | 渐进式同步 | 重要数据优先,其他数据按需同步 |
4.2 数据模型设计
4.2.1 服务端数据模型
服务端采用六表设计,详见第2.2.2.1节。核心表包括:
session_base_info:会话基本信息session_private_ext:单聊扩展信息session_group_ext:群聊扩展信息session_members:会话成员关系(最关键)session_operation_logs:操作日志session_ext_info:扩展信息
设计要点:
- 会话ID由服务端统一生成(单聊:
{user_id1}_{user_id2},群聊:{uuid}) - 每个用户在
session_members表中有独立的记录,管理个人偏好和未读数 - 通过
version字段实现乐观锁,支持离线同步和冲突解决
4.2.2 客户端数据模型
客户端使用SQLite存储会话缓存数据,用于离线访问和快速UI渲染。
客户端会话表 (sessions):
CREATE TABLE sessions (
sid TEXT PRIMARY KEY, -- 会话ID(与服务端一致)
session_type INTEGER NOT NULL, -- 会话类型:1-单聊,2-群聊,3-系统
target_id INTEGER, -- 目标ID(用户ID或群组ID)
target_name TEXT, -- 对方名称或群组名称
target_avatar TEXT, -- 对方头像或群组头像
last_message_id TEXT, -- 最后一条消息ID
last_message_content TEXT, -- 最后一条消息内容预览
last_message_type INTEGER, -- 最后一条消息类型
last_message_time INTEGER, -- 最后一条消息时间戳
unread_count INTEGER DEFAULT 0, -- 未读消息数(针对当前用户)
is_top INTEGER DEFAULT 0, -- 是否置顶(用户偏好):0-否,1-是
is_muted INTEGER DEFAULT 0, -- 是否静音(用户偏好):0-否,1-是
is_deleted INTEGER DEFAULT 0, -- 是否已删除(用户侧软删除):0-否,1-是
created_at INTEGER, -- 创建时间
updated_at INTEGER, -- 更新时间
sync_time INTEGER, -- 与服务端同步时间
server_version INTEGER DEFAULT 0 -- 服务端数据版本号
);
-- 创建索引
CREATE INDEX idx_session_type ON sessions(session_type);
CREATE INDEX idx_last_message_time ON sessions(last_message_time DESC);
CREATE INDEX idx_is_top ON sessions(is_top);
客户端同步状态表 (session_sync_state):
CREATE TABLE session_sync_state (
id INTEGER PRIMARY KEY AUTOINCREMENT,
last_sync_time INTEGER, -- 上次同步时间戳
last_sync_version INTEGER, -- 上次同步时的服务端版本号
pending_operations TEXT, -- 待同步的操作列表(JSON格式)
updated_at INTEGER -- 更新时间
);
数据分类与存储策略:
| 数据类型 | 存储位置 | 同步策略 | 说明 |
|---|---|---|---|
| 会话ID | 服务端+客户端 | 必须同步 | 服务端生成,客户端使用 |
| 会话类型 | 服务端+客户端 | 必须同步 | 确保多端一致 |
| 参与者信息 | 服务端+客户端 | 必须同步 | 群聊成员变化需同步 |
| 最后消息 | 服务端+客户端 | 实时同步 | 消息触发即时推送 |
| 未读数 | 服务端+客户端 | 实时同步 | 服务端计算,客户端展示 |
| 置顶状态 | 服务端+客户端 | 必须同步 | 用户偏好需多端一致 |
| 静音状态 | 服务端+客户端 | 必须同步 | 用户偏好需多端一致 |
| UI展开状态 | 客户端 | 本地存储 | 仅影响当前设备 |
| 滚动位置 | 客户端 | 本地存储 | 仅影响当前设备 |
| 排序偏好 | 客户端 | 本地存储 | 仅影响当前设备 |
4.3 核心流程设计
4.3.1 初始化与首次同步
流程说明:
- 应用启动,建立WebSocket连接
- 从本地数据库加载缓存的会话数据
- 通过HTTP请求从服务端拉取最新会话列表
- 合并本地数据和服务器数据(以服务器为准)
- 更新本地数据库和UI
📋 点击展开/收起 初始化流程代码示例
/**
* 前端部分(渲染进程) - 会话管理器初始化
*/
class SessionManager {
private sessions: Map<string, Session> = new Map();
private lastSyncTime: number = 0;
private lastSyncVersion: number = 0;
private isOnline: boolean = true;
/**
* 初始化会话管理器
*/
async initialize(): Promise<void> {
// 1. 从本地数据库加载会话
const localSessions = await this.loadSessionsFromLocalDB();
localSessions.forEach(session => {
this.sessions.set(session.sid, session);
});
// 2. 从本地数据库加载同步状态
const syncState = await this.loadSyncState();
if (syncState) {
this.lastSyncTime = syncState.lastSyncTime;
this.lastSyncVersion = syncState.lastSyncVersion;
}
// 3. 从服务端同步最新会话
await this.syncSessionsFromServer();
// 4. 更新UI
this.updateUI();
console.log('会话管理器初始化完成');
}
/**
* 从本地数据库加载会话
*/
private async loadSessionsFromLocalDB(): Promise<Session[]> {
return await window.ipcRenderer.invoke('sessions:getAll');
}
/**
* 从本地数据库加载同步状态
*/
private async loadSyncState(): Promise<SyncState | null> {
return await window.ipcRenderer.invoke('syncState:get');
}
/**
* 从服务端同步会话列表
*/
async syncSessionsFromServer(): Promise<void> {
try {
// 请求服务端会话列表(增量同步)
const response = await fetch(`/api/sessions?since=${this.lastSyncTime}&version=${this.lastSyncVersion}`);
if (!response.ok) {
throw new Error(`同步失败: ${response.statusText}`);
}
const data = await response.json();
// 更新本地会话
for (const session of data.sessions) {
this.updateOrInsertSession(session);
}
// 持久化到本地数据库
await this.saveSessionsToLocalDB();
// 更新同步状态
this.lastSyncTime = data.syncTime;
this.lastSyncVersion = data.syncVersion;
await this.saveSyncState();
// 更新UI
this.updateUI();
console.log(`同步了 ${data.sessions.length} 个会话`);
} catch (error) {
console.error('同步会话失败:', error);
// 同步失败时,使用本地缓存数据,不影响用户体验
}
}
/**
* 更新或插入会话
*/
private updateOrInsertSession(session: Session): void {
const existing = this.sessions.get(session.sid);
if (existing) {
// 版本检查:只更新服务端版本更高的数据
if (session.serverVersion > existing.serverVersion) {
this.sessions.set(session.sid, session);
}
} else {
// 新增会话
this.sessions.set(session.sid, session);
}
}
/**
* 保存会话到本地数据库
*/
private async saveSessionsToLocalDB(): Promise<void> {
const sessions = Array.from(this.sessions.values());
await window.ipcRenderer.invoke('sessions:batchSave', sessions);
}
/**
* 保存同步状态
*/
private async saveSyncState(): Promise<void> {
await window.ipcRenderer.invoke('syncState:save', {
lastSyncTime: this.lastSyncTime,
lastSyncVersion: this.lastSyncVersion
});
}
/**
* 更新UI
*/
private updateUI(): void {
const sortedSessions = this.getSortedSessions();
this.emit('sessions:updated', sortedSessions);
}
/**
* 获取排序后的会话列表
*/
private getSortedSessions(): Session[] {
return Array.from(this.sessions.values())
.filter(s => !s.isDeleted)
.sort((a, b) => {
// 置顶的排前面
if (a.isTop && !b.isTop) return -1;
if (!a.isTop && b.isTop) return 1;
// 按最后消息时间倒序
return b.lastMessageTime - a.lastMessageTime;
});
}
}
/**
* 前端部分(主进程) - SQLite会话服务
*/
@Injectable()
export class SessionStorageService {
constructor(
@InjectDatabase() private database: Database
) {}
/**
* 批量保存会话
*/
async saveSessions(sessions: Session[]): Promise<void> {
const stmt = await this.database.prepare(`
INSERT OR REPLACE INTO sessions (
sid, session_type, target_id, target_name, target_avatar,
last_message_id, last_message_content, last_message_type,
last_message_time, unread_count, is_top, is_muted, is_deleted,
created_at, updated_at, sync_time, server_version
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const session of sessions) {
await stmt.run(
session.sid,
session.sessionType,
session.targetId,
session.targetName,
session.targetAvatar,
session.lastMessageId,
session.lastMessageContent,
session.lastMessageType,
session.lastMessageTime,
session.unreadCount,
session.isTop ? 1 : 0,
session.isMuted ? 1 : 0,
session.isDeleted ? 1 : 0,
session.createdAt,
session.updatedAt,
Date.now(),
session.serverVersion
);
}
await stmt.finalize();
}
/**
* 获取所有会话
*/
async getAllSessions(): Promise<Session[]> {
const rows = await this.database.all(`
SELECT * FROM sessions WHERE is_deleted = 0
ORDER BY is_top DESC, last_message_time DESC
`);
return rows.map(this.mapRowToSession);
}
}
/**
* 前端部分(主进程) - 同步状态服务
*/
@Injectable()
export class SyncStateService {
constructor(
@InjectDatabase() private database: Database
) {}
/**
* 获取同步状态
*/
async getSyncState(): Promise<SyncState | null> {
const row = await this.database.get(`
SELECT * FROM session_sync_state WHERE id = 1
`);
if (!row) {
return null;
}
return {
lastSyncTime: row.last_sync_time,
lastSyncVersion: row.last_sync_version,
pendingOperations: JSON.parse(row.pending_operations || '[]')
};
}
/**
* 保存同步状态
*/
async saveSyncState(state: SyncState): Promise<void> {
await this.database.run(`
INSERT OR REPLACE INTO session_sync_state (id, last_sync_time, last_sync_version, pending_operations, updated_at)
VALUES (1, ?, ?, ?, ?)
`, [
state.lastSyncTime,
state.lastSyncVersion,
JSON.stringify(state.pendingOperations || []),
Date.now()
]);
}
}
/**
* 后端部分 - 同步服务
*/
@Injectable()
export class SyncService {
/**
* 增量同步会话列表
*/
async incrementalSync(userId: number, since: number, version: number): Promise<SyncResponse> {
// 1. 查询指定时间之后更新的会话
const sessions = await this.sessionRepository.getSessionsByUserIdSince(userId, since);
// 2. 过滤版本号更高的会话
const updatedSessions = sessions.filter(s => s.version > version);
// 3. 计算更新数量
const updatedCount = await this.sessionRepository.countUpdatedSessions(userId, since);
return {
sessions: updatedSessions,
syncTime: Date.now(),
syncVersion: this.getLatestSyncVersion(),
updatedCount
};
}
}
4.3.2 会话创建(显式创建)
流程说明:
- 用户点击联系人或新建会话按钮
- 客户端先检查本地是否已存在该会话
- 如果会话不存在,客户端请求服务端创建会话
- 服务端创建会话,生成会话ID
- 服务端推送新会话给所有参与者
- 客户端创建成功后才能发送消息
- 发送消息时需要带上会话ID
会话ID生成规则:
- 单聊会话ID:
min_user_id_max_user_id(例如:10001_10002) - 群聊会话ID:
uuid(例如:a1b2c3d4-e5f6-4789-0123-456789abcdef)
📋 点击展开/收起 会话创建代码示例
/**
* 前端部分(渲染进程) - 会话创建服务
*/
class SessionCreator {
/**
* 创建单聊会话
*/
async createPrivateSession(toUserId: number): Promise<string> {
// 1. 先检查本地是否已存在该会话
const existingSession = await this.checkSessionExists(toUserId, SessionType.Private);
if (existingSession) {
console.log('会话已存在:', existingSession.sid);
return existingSession.sid;
}
// 2. 请求服务端创建会话
try {
const response = await fetch('/api/sessions/private', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ toUserId })
});
if (!response.ok) {
throw new Error(`创建会话失败: ${response.statusText}`);
}
const result = await response.json();
// 3. 保存会话到本地
await this.saveSessionToLocal(result.session);
// 4. 更新本地会话Map
this.sessions.set(result.session.sid, result.session);
// 5. 更新UI
this.updateUI();
console.log('单聊会话创建成功:', result.session.sid);
return result.session.sid;
} catch (error) {
console.error('创建单聊会话失败:', error);
throw error;
}
}
/**
* 创建群聊会话
*/
async createGroupSession(groupName: string, memberIds: number[]): Promise<string> {
// 1. 请求服务端创建群聊
try {
const response = await fetch('/api/sessions/group', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
groupName,
memberIds
})
});
if (!response.ok) {
throw new Error(`创建群聊失败: ${response.statusText}`);
}
const result = await response.json();
// 2. 保存会话到本地
await this.saveSessionToLocal(result.session);
// 3. 更新本地会话Map
this.sessions.set(result.session.sid, result.session);
// 4. 更新UI
this.updateUI();
console.log('群聊会话创建成功:', result.session.sid);
return result.session.sid;
} catch (error) {
console.error('创建群聊会话失败:', error);
throw error;
}
}
/**
* 检查本地是否已存在会话
*/
private async checkSessionExists(targetId: number, sessionType: SessionType): Promise<Session | null> {
// 单聊:根据目标用户ID查找
if (sessionType === SessionType.Private) {
for (const session of this.sessions.values()) {
if (session.sessionType === SessionType.Private && session.targetId === targetId) {
return session;
}
}
}
return null;
}
}
/**
* 前端部分(渲染进程) - 消息发送服务(显式创建模式)
*/
class MessageSender {
/**
* 发送消息 - 显式创建会话模式
* 必须先创建会话,才能发送消息
*/
async sendMessage(sessionId: string, content: string, messageType: MessageType = MessageType.Text): Promise<void> {
// 1. 检查会话是否存在
const session = this.sessions.get(sessionId);
if (!session) {
throw new Error('会话不存在,请先创建会话');
}
// 2. 构造消息对象
const message: MessageDTO = {
fromId: getCurrentUserId(),
toId: session.targetId,
sessionId,
content,
type: messageType,
timestamp: Date.now()
};
try {
// 3. 通过WebSocket发送消息到服务端
const result = await window.ipcRenderer.invoke('ws:send', {
type: 'message',
data: message
});
console.log('消息发送成功,消息ID:', result.messageId);
} catch (error) {
console.error('消息发送失败:', error);
throw error;
}
}
}
/**
* 后端部分 - 会话创建服务
*/
@Injectable()
export class SessionCreationService {
constructor(
private sessionService: SessionService,
private sessionMembersRepository: SessionMembersRepository
) {}
/**
* 创建单聊会话
*/
async createPrivateSession(dto: CreatePrivateSessionDTO): Promise<Session> {
const currentUserId = getCurrentUserId();
const { toUserId } = dto;
// 1. 生成会话ID(小用户ID_大用户ID)
const userIds = [currentUserId, toUserId].sort((a, b) => a - b);
const sessionId = `${userIds[0]}_${userIds[1]}`;
// 2. 检查会话是否已存在
const existing = await this.sessionService.getSessionById(sessionId);
if (existing) {
throw new BadRequestException('会话已存在');
}
// 3. 保存会话基本信息
await this.sessionService.createBaseSession({
sessionId,
sessionType: SessionType.Private
});
// 4. 保存单聊扩展信息
await this.sessionService.createPrivateSessionExt({
sessionId,
userId1: userIds[0],
userId2: userIds[1]
});
// 5. 创建会话成员(两个用户)
await this.sessionService.createMember({
sessionId,
userId: currentUserId,
role: MemberRole.Member
});
await this.sessionService.createMember({
sessionId,
userId: toUserId,
role: MemberRole.Member
});
// 6. 获取完整的会话信息
const session = await this.sessionService.getSessionById(sessionId);
// 7. 推送新会话给所有参与者
await this.sessionService.pushNewSession(session, [currentUserId, toUserId]);
return session;
}
/**
* 创建群聊会话
*/
async createGroupSession(dto: CreateGroupSessionDTO): Promise<Session> {
const currentUserId = getCurrentUserId();
const { groupName, memberIds } = dto;
// 1. 生成群聊会话ID(UUID)
const sessionId = this.generateUUID();
// 2. 保存会话基本信息
await this.sessionService.createBaseSession({
sessionId,
sessionType: SessionType.Group
});
// 3. 保存群聊扩展信息
await this.sessionService.createGroupSessionExt({
sessionId,
groupName,
groupAvatar: '',
announcement: '',
maxMembers: 500,
joinPermission: 1,
messagePermission: 1
});
// 4. 创建会话成员
const allMemberIds = [currentUserId, ...memberIds];
for (let i = 0; i < allMemberIds.length; i++) {
const userId = allMemberIds[i];
const role = i === 0 ? MemberRole.Owner : MemberRole.Member;
await this.sessionService.createMember({
sessionId,
userId,
role
});
}
// 5. 获取完整的会话信息
const session = await this.sessionService.getSessionById(sessionId);
// 6. 推送新会话给所有成员
await this.sessionService.pushNewSession(session, allMemberIds);
return session;
}
/**
* 生成UUID
*/
private generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
}
/**
* 后端部分 - 消息处理服务(显式创建模式)
*/
@Injectable()
export class MessageService {
constructor(
private messageRepository: MessageRepository,
private sessionService: SessionService,
private unreadService: UnreadService
) {}
/**
* 处理消息(会话ID必须存在)
*/
async handleMessage(message: MessageDTO): Promise<void> {
// 1. 验证会话ID是否存在
if (!message.sessionId) {
throw new BadRequestException('会话ID不能为空,请先创建会话');
}
// 2. 验证会话是否存在
const session = await this.sessionService.getSessionById(message.sessionId);
if (!session) {
throw new BadRequestException('会话不存在');
}
// 3. 验证用户是否是会话成员
const isMember = await this.sessionService.isUserInSession(message.sessionId, message.fromId);
if (!isMember) {
throw new BadRequestException('您不是该会话的成员');
}
// 4. 保存消息
const savedMessage = await this.messageRepository.save({
...message
});
// 5. 更新会话的最后消息
await this.sessionService.updateLastMessage(message.sessionId, {
messageId: savedMessage.id,
content: message.content,
type: message.type,
timestamp: message.timestamp
});
// 6. 增加接收方的未读数(群聊需要给所有成员增加未读数,除了发送方)
if (session.sessionType === SessionType.Private) {
// 单聊:只给接收方增加未读数
await this.unreadService.incrementUnreadCount(message.sessionId, message.toId);
} else {
// 群聊:给所有成员增加未读数,除了发送方
const members = await this.sessionService.getMembersBySessionId(message.sessionId);
for (const member of members) {
if (member.userId !== message.fromId) {
await this.unreadService.incrementUnreadCount(message.sessionId, member.userId);
}
}
}
// 7. 推送消息给接收方
await this.pushMessageToReceiver(savedMessage, session.sessionType);
// 8. 推送会话更新给所有参与者
await this.sessionService.pushSessionUpdate(message.sessionId);
return savedMessage;
}
}
4.3.3 会话更新(实时推送)
流程说明:
- 服务端检测到会话变更(新消息、未读数变化、置顶等)
- 服务端通过WebSocket推送会话更新给所有参与者
- 客户端接收WebSocket消息,解析会话更新事件
- 客户端更新本地会话数据
- 客户端持久化到本地数据库
- 客户端更新UI
📋 点击展开/收起 会话更新代码示例
/**
* 前端部分(渲染进程) - WebSocket监听会话更新
*/
class SessionManager {
private wsConnection: WebSocket | null = null;
/**
* 连接WebSocket
*/
connectWebSocket(): void {
this.wsConnection = new WebSocket('ws://your-server/ws');
this.wsConnection.onmessage = (event) => {
const message = JSON.parse(event.data);
switch (message.type) {
case 'session_update':
this.handleSessionUpdate(message.data);
break;
case 'new_session':
this.handleNewSession(message.data);
break;
default:
break;
}
};
}
/**
* 处理会话更新事件
*/
handleSessionUpdate(event: SessionUpdateEvent): void {
const { sid, update } = event;
const session = this.sessions.get(sid);
if (session) {
// 更新现有会话
Object.assign(session, update);
this.saveSessionToLocalDB(session);
this.updateUI();
} else {
// 新增会话
const newSession: Session = {
sid,
...update,
createdAt: Date.now()
};
this.sessions.set(sid, newSession);
this.saveSessionToLocalDB(newSession);
this.updateUI();
}
}
/**
* 保存单个会话到本地数据库
*/
private async saveSessionToLocalDB(session: Session): Promise<void> {
await window.ipcRenderer.invoke('sessions:save', session);
}
}
/**
* 后端部分 - WebSocket网关
* 用于处理客户端WebSocket连接和消息推送
*/
@WebSocketGateway()
export class SessionWebSocketGateway {
constructor(
private eventBus: EventBus,
private webSocketGateway: SessionWebSocketGateway
) {}
@SubscribeMessage('session_update')
handleSessionUpdate(client: Socket, payload: SessionUpdateEvent): void {
// 转发会话更新到渲染进程
this.eventBus.emit('session_update', payload);
}
/**
* 向指定用户发送WebSocket消息
*/
sendToUser(userId: number, message: any): void {
// 实现向指定用户发送消息的逻辑
}
}
/**
* 后端部分 - 会话服务
*/
@Injectable()
export class SessionService {
constructor(
private sessionRepository: SessionRepository,
private webSocketGateway: SessionWebSocketGateway
) {}
/**
* 推送会话更新给所有参与者
*/
async pushSessionUpdate(sessionId: string): Promise<void> {
// 1. 获取会话信息
const session = await this.sessionRepository.getSessionById(sessionId);
if (!session) {
return;
}
// 2. 获取会话成员
const members = await this.sessionMembersRepository.getMembersBySessionId(sessionId);
// 3. 推送会话更新给所有成员
for (const member of members) {
this.webSocketGateway.sendToUser(member.userId, {
type: 'session_update',
data: {
sid: session.sessionId,
update: {
lastMessageId: session.lastMessageId,
lastMessageContent: session.lastMessageContent,
lastMessageType: session.lastMessageType,
lastMessageTime: session.lastMessageTime,
unreadCount: member.unreadCount,
isTop: member.isTop,
isMuted: member.isMuted,
serverVersion: session.version
}
}
});
}
}
}
4.3.4 未读数管理
核心设计原则:
- 服务端计算未读数:确保多端未读数一致
- 实时推送更新:未读数变化时立即推送给所有客户端
- 精确计数:每个用户独立计数,支持离线状态下的准确计数
📋 点击展开/收起 未读数管理代码示例
/**
* 前端部分(渲染进程) - 标记已读
*/
class SessionManager {
/**
* 标记会话为已读
*/
async markSessionAsRead(sid: string): Promise<void> {
const session = this.sessions.get(sid);
if (!session || session.unreadCount === 0) {
return;
}
try {
// 1. 请求服务端标记已读
await fetch(`/api/sessions/${sid}/read`, {
method: 'POST'
});
// 2. 服务端会推送会话更新,所以这里只需等待
console.log('已发送标记已读请求');
} catch (error) {
console.error('标记已读失败:', error);
// 网络错误时,先更新本地UI,等待下次同步
session.unreadCount = 0;
this.updateUI();
}
}
}
/**
* 后端部分 - 未读数管理
*/
@Injectable()
export class UnreadService {
/**
* 增加未读数(收到新消息时调用)
*/
async incrementUnreadCount(sessionId: string, userId: number): Promise<void> {
// 1. 在session_members表中增加未读数
await this.sessionMembersRepository.incrementUnreadCount(sessionId, userId);
// 2. 如果用户删除了该会话,需要恢复会话
await this.sessionMembersRepository.restoreForMember(sessionId, userId);
// 推送会话更新
await this.sessionService.pushSessionUpdate(sessionId);
}
/**
* 清零未读数(标记已读时调用)
*/
async markAsRead(sessionId: string, userId: number): Promise<void> {
// 1. 在session_members表中清零未读数
await this.sessionMembersRepository.resetUnreadCount(sessionId, userId);
// 推送会话更新
await this.sessionService.pushSessionUpdate(sessionId);
}
}
4.3.5 用户偏好管理(置顶/静音)
核心设计原则:
- 用户级偏好:每个用户的置顶和静音状态独立管理
- 服务端存储:用户偏好存储在服务端,确保多端一致
- 实时同步:偏好变更时通过WebSocket实时推送给所有客户端
📋 点击展开/收起 用户偏好管理代码示例
/**
* 前端部分(渲染进程) - 用户偏好管理
*/
class SessionManager {
/**
* 置顶会话
*/
async toggleSessionTop(sid: string, isTop: boolean): Promise<void> {
try {
// 通过HTTP请求服务端更新置顶状态
await fetch(`/api/sessions/${sid}/top`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ isTop })
});
console.log('置顶状态更新成功');
} catch (error) {
console.error('置顶失败:', error);
throw error;
}
// 服务端会通过WebSocket推送会话更新,客户端接收后自动更新UI
}
/**
* 静音会话
*/
async toggleSessionMute(sid: string, isMuted: boolean): Promise<void> {
try {
// 通过HTTP请求服务端更新静音状态
await fetch(`/api/sessions/${sid}/mute`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ isMuted })
});
console.log('静音状态更新成功');
} catch (error) {
console.error('静音失败:', error);
throw error;
}
// 服务端会通过WebSocket推送会话更新,客户端接收后自动更新UI
}
}
/**
* 后端部分 - 会话偏好管理服务
*/
@Injectable()
export class SessionPreferenceService {
constructor(
private sessionMembersRepository: SessionMembersRepository,
private sessionOperationLogsRepository: SessionOperationLogsRepository,
private sessionService: SessionService
) {}
/**
* 切换置顶状态
*/
async toggleSessionTop(sessionId: string, userId: number, isTop: boolean): Promise<void> {
// 1. 更新会话成员的置顶状态
await this.sessionMembersRepository.updateTopStatus(sessionId, userId, isTop);
// 2. 保存操作日志
await this.sessionOperationLogsRepository.save({
sessionId,
operatorId: userId,
operationType: isTop ? 1 : 2, // 1-置顶, 2-取消置顶
operationTime: Date.now()
});
// 3. 推送会话更新给所有成员
await this.sessionService.pushSessionUpdate(sessionId);
}
/**
* 切换静音状态
*/
async toggleSessionMute(sessionId: string, userId: number, isMuted: boolean): Promise<void> {
// 1. 更新会话成员的静音状态
await this.sessionMembersRepository.updateMuteStatus(sessionId, userId, isMuted);
// 2. 保存操作日志
await this.sessionOperationLogsRepository.save({
sessionId,
operatorId: userId,
operationType: isMuted ? 3 : 4, // 3-静音, 4-取消静音
operationTime: Date.now()
});
// 3. 推送会话更新给所有成员
await this.sessionService.pushSessionUpdate(sessionId);
}
}
4.3.6 会话删除(软删除)
核心设计原则:
- 用户级删除:每个用户可以独立删除会话,不影响其他用户
- 软删除:只标记会话为已删除,保留历史消息
- 恢复功能:用户可以恢复已删除的会话
- 自动恢复:收到新消息时自动恢复已删除的会话
📋 点击展开/收起 会话删除代码示例
/**
* 前端部分(渲染进程) - 会话删除
*/
class SessionManager {
/**
* 删除会话
*/
async deleteSession(sid: string): Promise<void> {
// 1. 请求服务端删除会话
await fetch(`/api/sessions/${sid}`, {
method: 'DELETE'
});
// 2. 从本地Map中移除
const session = this.sessions.get(sid);
if (session) {
session.isDeleted = true;
// 3. 持久化到本地数据库
await this.saveSessionToLocalDB(session);
// 4. 更新UI
this.updateUI();
console.log('会话已删除');
}
}
/**
* 恢复已删除的会话
*/
async restoreSession(sid: string): Promise<void> {
// 1. 请求服务端恢复会话
await fetch(`/api/sessions/${sid}/restore`, {
method: 'POST'
});
// 2. 更新本地会话
const session = this.sessions.get(sid);
if (session) {
session.isDeleted = false;
// 3. 持久化到本地数据库
await this.saveSessionToLocalDB(session);
// 4. 更新UI
this.updateUI();
console.log('会话已恢复');
}
}
}
/**
* 后端部分 - 会话删除服务
*/
@Injectable()
export class SessionDeleteService {
/**
* 删除会话(软删除)
*/
async deleteSession(sessionId: string, userId: number): Promise<void> {
// 1. 在session_members表中设置is_deleted = true
await this.sessionMembersRepository.markAsDeleted(sessionId, userId);
// 2. 保存操作日志
await this.sessionOperationLogsRepository.save({
sessionId,
operatorId: userId,
operationType: 5, // 5-删除
operationTime: Date.now()
});
// 3. 推送会话更新
await this.sessionService.pushSessionUpdate(sessionId);
}
/**
* 恢复会话
*/
async restoreSession(sessionId: string, userId: number): Promise<void> {
// 1. 在session_members表中设置is_deleted = false
await this.sessionMembersRepository.restoreMember(sessionId, userId);
// 2. 保存操作日志
await this.sessionOperationLogsRepository.save({
sessionId,
operatorId: userId,
operationType: 6, // 6-恢复删除
operationTime: Date.now()
});
// 3. 推送会话更新
await this.sessionService.pushSessionUpdate(sessionId);
}
}
4.3.7 离线同步与冲突解决
核心设计原则:
- 乐观锁:通过
version字段实现冲突检测 - 增量同步:定期拉取变更数据,减少网络传输
- 冲突解决:服务端数据优先,本地数据作为备份
- 操作队列:离线时的操作入队,上线后自动同步
📋 点击展开/收起 离线同步代码示例
/**
* 前端部分(渲染进程) - 离线同步
*/
class SessionManager {
/**
* 处理网络状态变化
*/
handleNetworkChange(isOnline: boolean): void {
this.isOnline = isOnline;
if (isOnline) {
// 网络恢复,执行离线同步
this.syncOfflineData();
}
}
/**
* 同步离线数据
*/
async syncOfflineData(): Promise<void> {
// 1. 获取待同步的操作
const pendingOperations = await this.loadPendingOperations();
// 2. 执行同步
await this.syncPendingOperations(pendingOperations);
// 3. 从服务端拉取最新数据
await this.syncSessionsFromServer();
console.log('数据同步完成');
}
/**
* 全量同步从服务端
*/
private async fullSyncFromServer(): Promise<void> {
try {
// 请求服务端会话列表(全量)
const response = await fetch('/api/sessions/fullSync');
if (!response.ok) {
throw new Error(`同步失败: ${response.statusText}`);
}
const data = await response.json();
// 清空本地会话
this.sessions.clear();
// 更新本地会话
for (const serverSession of data.sessions) {
const session: Session = {
sid: serverSession.sid,
sessionType: serverSession.session_type,
targetId: serverSession.target_id,
targetName: serverSession.target_name,
targetAvatar: serverSession.target_avatar,
lastMessageId: serverSession.last_message_id,
lastMessageContent: serverSession.last_message_content,
lastMessageType: serverSession.last_message_type,
lastMessageTime: serverSession.last_message_time,
unreadCount: serverSession.unread_count,
isTop: serverSession.is_top === 1,
isMuted: serverSession.is_muted === 1,
isDeleted: serverSession.is_deleted === 1,
createdAt: serverSession.created_at,
updatedAt: serverSession.updated_at,
syncTime: Date.now(),
serverVersion: serverSession.server_version
};
this.sessions.set(session.sid, session);
}
// 持久化到本地数据库
await this.saveSessionsToLocalDB();
// 更新同步状态
this.lastSyncTime = data.syncTime;
this.lastSyncVersion = data.syncVersion;
await this.saveSyncState();
console.log(`全量同步完成:共 ${data.sessions.length} 个会话`);
} catch (error) {
console.error('全量同步失败:', error);
}
}
/**
* 同步待同步的操作
*/
private async syncPendingOperations(operations: PendingOperation[]): Promise<void> {
const failedOps: PendingOperation[] = [];
const successOps: PendingOperation[] = [];
for (const op of operations) {
try {
switch (op.type) {
case 'top':
await this.toggleSessionTop(op.sessionId, op.isTop);
successOps.push(op);
break;
case 'mute':
await this.toggleSessionMute(op.sessionId, op.isMuted);
successOps.push(op);
break;
case 'delete':
await this.deleteSession(op.sessionId);
successOps.push(op);
break;
}
} catch (error) {
console.error(`同步操作失败 [${op.type}]:`, error);
failedOps.push(op);
// 重试策略:可以根据失败次数决定是否继续重试
if (op.retryCount && op.retryCount > 3) {
// 超过重试次数限制的操作
console.error(`操作 ${op.sessionId} 超过最大重试次数,放弃同步`);
} else {
// 保留到待同步队列,下次上线继续重试
}
}
}
// 只清空成功同步的操作
await window.ipcRenderer.invoke('syncState:savePendingOps', failedOps);
console.log(`同步完成:成功 ${successOps.length} 个,失败 ${failedOps.length} 个`);
}
/**
* 保存待同步的操作(离线时调用)
*/
private async savePendingOperation(op: PendingOperation): Promise<void> {
const pendingOps = await this.loadPendingOperations();
pendingOps.push(op);
await window.ipcRenderer.invoke('syncState:savePendingOps', pendingOps);
}
/**
* 清空待同步的操作(用于同步完成后)
*/
private async clearPendingOperations(): Promise<void> {
await window.ipcRenderer.invoke('syncState:savePendingOps', []);
}
/**
* 加载待同步的操作
*/
private async loadPendingOperations(): Promise<PendingOperation[]> {
const syncState = await this.loadSyncState();
return syncState?.pendingOperations || [];
}
}
/**
* 后端部分 - 离线同步服务
*/
@Injectable()
export class SyncService {
/**
* 全量同步会话列表
*/
async fullSync(userId: number): Promise<FullSyncResponse> {
// 1. 查询用户的所有会话
const sessions = await this.sessionRepository.getSessionsByUserId(userId);
// 2. 获取当前同步版本号
const syncVersion = await this.getLatestSyncVersion();
const syncTime = Date.now();
return {
sessions,
syncTime,
syncVersion
};
}
/**
* 增量同步会话列表
*/
async incrementalSync(userId: number, since: number, version: number): Promise<SyncResponse> {
// 1. 查询指定时间之后更新的会话
const sessions = await this.sessionRepository.getSessionsByUserIdSince(userId, since);
// 2. 过滤版本号更高的会话
const updatedSessions = sessions.filter(s => s.version > version);
// 3. 计算更新数量
const updatedCount = await this.sessionRepository.countUpdatedSessions(userId, since);
return {
sessions: updatedSessions,
syncTime: Date.now(),
syncVersion: this.getLatestSyncVersion(),
updatedCount
};
}
/**
* 获取最新的同步版本号
*/
private async getLatestSyncVersion(): Promise<number> {
const result = await this.sessionRepository.getLatestVersion();
return result || 0;
}
}
4.4 数据类型定义
/**
* 会话类型
*/
enum SessionType {
Private = 1, // 单聊
Group = 2, // 群聊
System = 3 // 系统会话
}
/**
* 场景类型(用于区分单聊和群聊场景)
*/
enum Scene {
PrivateChat = 1, // 单聊场景
GroupChat = 2 // 群聊场景
}
/**
* 消息类型
*/
enum MessageType {
Text = 1, // 文本消息
Image = 2, // 图片消息
Audio = 3, // 音频消息
Video = 4, // 视频消息
File = 5, // 文件消息
Location = 6, // 位置消息
Custom = 99 // 自定义消息
}
/**
* 会话成员角色
*/
enum MemberRole {
Member = 1, // 普通成员
Admin = 2, // 管理员
Owner = 3 // 群主
}
/**
* 消息数据传输对象
*
* 说明:
* - 显式创建模式:sessionId为必填字段,toId可选(从session中获取)
* - 隐式创建模式:sessionId为可选字段(服务端自动生成),toId必填
* - groupName, groupAvatar, members等群聊信息应从session表或扩展表获取,不包含在消息中
*/
interface MessageDTO {
fromId: number; // 发送方用户ID
toId?: number; // 接收方用户ID(仅单聊隐式创建时使用)
sessionId?: string; // 会话ID(显式创建时必填,隐式创建时由服务端返回)
content: string; // 消息内容
type: MessageType; // 消息类型
timestamp: number; // 时间戳
// 注:群聊相关信息(groupName, groupAvatar, members)应从session表或扩展表获取,
// 不包含在MessageDTO中,避免数据冗余和混乱
}
/**
* 会话数据对象
*/
interface Session {
sid: string; // 会话ID
sessionType: SessionType; // 会话类型
targetId: number; // 目标ID
targetName: string; // 目标名称
targetAvatar: string; // 目标头像
lastMessageId: string; // 最后一条消息ID
lastMessageContent: string; // 最后一条消息内容
lastMessageType: MessageType; // 最后一条消息类型
lastMessageTime: number; // 最后一条消息时间戳
unreadCount: number; // 未读数
isTop: boolean; // 是否置顶
isMuted: boolean; // 是否静音
isDeleted: boolean; // 是否已删除
createdAt: number; // 创建时间
updatedAt: number; // 更新时间
syncTime: number; // 同步时间
serverVersion: number; // 服务端版本号
}
/**
* 会话更新事件
*/
interface SessionUpdateEvent {
sid: string; // 会话ID
update: Partial<Session>; // 更新的字段
}
/**
* 同步状态
*/
interface SyncState {
lastSyncTime: number; // 上次同步时间
lastSyncVersion: number; // 上次同步版本号
pendingOperations: PendingOperation[]; // 待同步的操作
}
/**
* 待同步的操作
*/
interface PendingOperation {
type: 'top' | 'mute' | 'delete'; // 操作类型
sessionId: string; // 会话ID
isTop?: boolean; // 置顶状态
isMuted?: boolean; // 静音状态
timestamp: number; // 操作时间
retryCount?: number; // 重试次数(默认为0)
}
/**
* 同步响应
*/
interface SyncResponse {
sessions: Session[]; // 会话列表
syncTime: number; // 同步时间
syncVersion: number; // 同步版本号
updatedCount: number; // 更新数量
}
五、总结
本文档全面介绍了WebSocket会话管理方案的设计思路、技术选型和最佳实践。以下是对整个文档的核心要点总结。
5.1 会话管理核心概念
会话定义:
- 会话是指用户与另一个用户(单聊)或多个用户(群聊)之间的对话关系
- 每个会话都有唯一的标识符(sessionId),由服务端生成
- 会话包含:会话ID、参与者、最后一条消息、未读数、活跃时间等元信息
会话类型:
- 单聊会话:一对一聊天,参与者固定为2人
- 群聊会话:多人聊天,参与者可变
- 系统会话:系统通知、客服机器人等
会话核心功能:
- 会话查询:获取用户的会话列表,支持分页、搜索、过滤
- 会话创建:用户主动创建会话(显式创建),发送消息时需要先创建会话
- 会话更新:收到新消息时更新会话的最后消息和时间
- 会话删除:用户删除会话时处理,支持软删除或硬删除
- 会话排序:按最后消息时间、置顶状态、未读数等维度进行排序
- 未读数计算:实时统计每个会话的未读消息数
- 离线同步:断线重连后同步会话状态
- 多设备同步:跨设备同步会话状态
5.2 数据模型设计
服务端六表设计方案:
| 表名 | 核心职责 | 关键字段 |
|---|---|---|
session_base_info | 存储会话核心元数据 | session_id, session_type, last_message_* |
session_private_ext | 单聊特有信息 | user_id1, user_id2 |
session_group_ext | 群聊特有信息 | group_name, group_avatar, announcement |
session_members | 用户与会话关联 | user_id, unread_count, is_top, is_muted, is_deleted |
session_operation_logs | 操作审计 | operator_id, operation_type, operation_time |
session_ext_info | 灵活扩展 | key, value |
设计亮点:
- 职责分离:每个表专注于单一职责,避免数据冗余
- 用户级状态:
session_members表让每个用户的偏好独立管理 - 扩展性:
session_ext_info为未来功能预留空间 - 审计追踪:
session_operation_logs提供完整操作历史 - 数据一致性:外键约束和级联删除确保数据完整性
5.3 会话ID生成规则
核心原则:所有会话ID均由服务端生成,客户端不参与任何ID生成逻辑
生成规则:
单聊会话ID: {user_id1}_{user_id2}
- user_id1: 较小的用户ID
- user_id2: 较大的用户ID
- 使用下划线(_)作为分隔符
- 示例: 10001_10002(用户10001和用户10002的会话)
群聊会话ID: {uuid}
- 使用36位UUID v4格式
- 不添加任何前缀
- 示例: a1b2c3d4-e5f6-4789-0123-456789abcdef
为什么服务端生成:
- 跨端一致性:确保多个客户端对同一会话使用相同的ID
- 避免冲突:多端同时创建会话时,服务端统一生成ID避免冲突
- 数据权威:会话元数据由服务端管理,会话ID也应由服务端生成
- 易于管理:服务端集中管理会话ID,便于后续的会话查询和管理
5.4 方案选型对比
三种主要方案:
| 对比维度 | 方案一:传统企业级IM方案 | 方案二:轻量级实时IM方案 | 方案三:混合弹性IM方案(自定义模式) |
|---|---|---|---|
| 适用场景 | 大型企业应用,多设备同步,高一致性要求 | 中小型应用,实时性要求高,云端存储为主 | 需要平衡各种需求,业务复杂度高 |
| 会话创建 | 隐式创建 | 隐式创建 | 显式创建 |
| 数据存储 | 混合存储 | 云端存储为主 | 混合存储,重要数据服务端,辅助数据客户端 |
| 状态管理 | 服务端计算关键状态 | 云端计算状态 | 混合计算,关键状态服务端,UI状态客户端 |
| 同步机制 | 实时同步 + 按需同步 | 实时同步 | 实时同步 + 定时同步 + 按需同步 |
| 数据一致性 | 强一致性 | 云端强一致性 | 最终一致性 |
| 离线处理 | 渐进式同步 | 在线优先 | 渐进式同步 |
| 优点 | 数据一致性高,多设备自动同步 用户体验流畅 支持离线使用 安全性高 | 实时性极佳 客户端轻量级 多设备体验一致 云端数据安全可靠 | 灵活性强,可适应不同业务场景 平衡各种需求,兼顾性能和一致性 扩展性好,支持复杂业务逻辑 用户体验良好,响应迅速 |
| 缺点 | 服务端负载较重 网络依赖性强 实现复杂度较高 | 离线体验差 网络依赖性强 服务端压力大 | 实现最复杂 维护成本高 需要精心设计 |
推荐方案:方案三(混合弹性IM方案)适合大多数企业级IM应用,能够平衡各种需求,兼顾性能和一致性。
5.5 方案三核心设计要点
维度组合:
| 维度 | 方案 | 说明 |
|---|---|---|
| 会话创建 | 显式创建 | 发送消息前先创建会话 |
| 数据存储 | 混合存储 | 重要数据服务端,辅助数据客户端 |
| 状态管理 | 混合计算 | 关键状态服务端,UI状态客户端 |
| 同步机制 | 实时+按需 | WebSocket推送 + HTTP拉取 |
| 数据一致性 | 最终一致性 | 允许短暂不一致,定期同步 |
| 离线处理 | 渐进式同步 | 重要数据优先,其他数据按需同步 |
核心流程:
-
初始化与首次同步
- 应用启动,建立WebSocket连接
- 从本地数据库加载缓存的会话数据
- 通过HTTP请求从服务端拉取最新会话列表
- 合并本地数据和服务器数据(以服务器为准)
- 更新本地数据库和UI
-
会话创建(显式创建)
- 用户点击联系人或新建会话按钮
- 客户端先检查本地是否已存在该会话
- 如果会话不存在,客户端请求服务端创建会话
- 服务端创建会话,生成会话ID
- 服务端推送新会话给所有参与者
- 客户端创建成功后才能发送消息
- 发送消息时需要带上会话ID
-
会话更新(实时推送)
- 服务端检测到会话变更(新消息、未读数变化、置顶等)
- 服务端通过WebSocket推送会话更新给所有参与者
- 客户端接收WebSocket消息,解析会话更新事件
- 客户端更新本地会话数据
- 客户端持久化到本地数据库
- 客户端更新UI
-
未读数管理
- 服务端计算未读数,确保多端一致
- 实时推送更新,未读数变化时立即推送
- 精确计数,每个用户独立计数
-
用户偏好管理(置顶/静音)
- 用户级偏好,每个用户独立管理
- 服务端存储,确保多端一致
- 实时同步,偏好变更时立即推送
-
会话删除(软删除)
- 用户级删除,每个用户可以独立删除会话
- 软删除,只标记会话为已删除,保留历史消息
- 恢复功能,用户可以恢复已删除的会话
- 自动恢复,收到新消息时自动恢复已删除的会话
-
离线同步与冲突解决
- 乐观锁,通过
version字段实现冲突检测 - 增量同步,定期拉取变更数据
- 冲突解决,服务端数据优先,本地数据作为备份
- 操作队列,离线时的操作入队,上线后自动同步
- 乐观锁,通过
5.6 数据分类与存储策略
| 数据类型 | 存储位置 | 同步策略 | 说明 |
|---|---|---|---|
| 会话ID | 服务端+客户端 | 必须同步 | 服务端生成,客户端使用 |
| 会话类型 | 服务端+客户端 | 必须同步 | 确保多端一致 |
| 参与者信息 | 服务端+客户端 | 必须同步 | 群聊成员变化需同步 |
| 最后消息 | 服务端+客户端 | 实时同步 | 消息触发即时推送 |
| 未读数 | 服务端+客户端 | 实时同步 | 服务端计算,客户端展示 |
| 置顶状态 | 服务端+客户端 | 必须同步 | 用户偏好需多端一致 |
| 静音状态 | 服务端+客户端 | 必须同步 | 用户偏好需多端一致 |
| UI展开状态 | 客户端 | 本地存储 | 仅影响当前设备 |
| 滚动位置 | 客户端 | 本地存储 | 仅影响当前设备 |
| 排序偏好 | 客户端 | 本地存储 | 仅影响当前设备 |
5.7 核心设计原则
-
服务端主导:所有会话数据由服务端统一管理和生成,服务端作为权威数据源
-
客户端智能缓存:客户端缓存会话数据用于离线访问和快速响应,减少与服务端的频繁交互
-
实时+按需同步:通过WebSocket实时推送关键变更,按需拉取完整数据,平衡实时性和性能
-
最终一致性:允许短暂的数据不一致,通过同步机制保证最终一致
-
用户级状态管理:每个用户在
session_members表中有独立的记录,管理个人偏好和未读数 -
乐观锁机制:通过
version字段实现冲突检测和解决 -
操作日志审计:所有操作都通过
session_operation_logs审计追踪,便于回滚和追溯
5.8 实施建议
核心实现要点:
- 服务端统一生成和管理会话ID
- 客户端不参与ID生成,直接使用服务端返回的会话ID
- 用户主动创建会话(显式创建),发送消息前必须先创建会话
- 通过WebSocket实时推送会话更新
- 支持离线使用,网络恢复后自动同步
- 实现多设备数据一致性保证
实施步骤:
- 设计服务端会话表(六表设计)
- 实现服务端会话ID生成逻辑(单聊:
{user_id1}_{user_id2},群聊:{uuid}) - 实现会话的显式创建机制(客户端请求创建,服务端生成ID并返回)
- 实现消息发送时的会话ID验证机制
- 实现WebSocket实时推送
- 实现客户端缓存和离线同步
- 实现多设备数据一致性保证
- 实现未读数管理和用户偏好管理
- 实现会话删除和恢复功能
免责声明
- 技术文档性质:本文档为技术方案设计文档,内容基于通用技术实践和业界最佳实践编写
- 内容声明:文档中的技术方案、架构设计、代码示例等内容均为通用技术实现,不涉及任何特定公司或项目的商业机密、专利技术或内部架构
- 参考性质:本文档仅供技术参考和学习使用,不构成任何商业建议或技术实施承诺
- 使用风险:读者应根据自身项目的具体需求对本文档内容进行调整和优化,作者不对因使用本文档内容而造成的任何直接或间接损失承担责任
- 第三方引用:本文档引用的第三方技术文章、开源项目、API文档等均为公开资料,引用时已注明出处
版权声明
本文档内容为原创技术文档,仅供学习交流使用。文档中的代码示例、架构设计等技术内容为通用技术实践,不涉及任何特定公司的商业机密。如需引用本文档内容,请注明出处。