聊天系统迁移设计方案
一、系统概览
1.1 功能范围
本聊天系统支持以下功能,可按需裁剪迁移:
| 功能 |
说明 |
建议 |
| 私聊 |
用户间一对一消息 |
必须 |
| 群聊 |
群组消息,支持创建/加入/退出群组 |
必须 |
| 系统通知 |
后台推送通知给指定用户 |
必须 |
| 消息历史 |
私聊/群聊历史记录分页查询 |
必须 |
| 会话列表 |
用户会话列表(含未读数) |
必须 |
| 已读标记 |
标记会话已读 |
可选 |
| 在线状态 |
查询用户是否在线 |
可选 |
1.2 技术栈
| 层级 |
技术选型 |
说明 |
| 后端框架 |
Spring Boot 2.0.9+ |
支持 WebSocket + MyBatis-Plus |
| 通信协议 |
WebSocket + REST |
实时消息走 WebSocket,历史/列表走 REST |
| 数据持久化 |
MySQL 5.7+ |
4张核心表 |
| 缓存/会话 |
Redis |
JWT 校验 + 会话状态 |
| 前端框架 |
Vue 2.6.10 |
组件化实现,可适配其他 Vue 版本 |
二、数据库设计
2.1 建表 SQL
-- =============================================
-- 聊天系统数据库表
-- 编码: UTF-8MB4
-- =============================================
-- 1. 消息表
CREATE TABLE `chat_message` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '消息ID',
`message_type` TINYINT NOT NULL COMMENT '消息类型: 1=私聊, 2=群聊, 3=系统通知',
`sender_id` VARCHAR(64) NOT NULL COMMENT '发送者用户ID',
`receiver_id` VARCHAR(64) DEFAULT NULL COMMENT '接收者用户ID(私聊)',
`group_id` VARCHAR(64) DEFAULT NULL COMMENT '群组ID(群聊)',
`content` TEXT NOT NULL COMMENT '消息内容',
`content_type` TINYINT DEFAULT 1 COMMENT '内容类型: 1=文本, 2=图片, 3=文件',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
INDEX `idx_sender` (`sender_id`),
INDEX `idx_receiver` (`receiver_id`),
INDEX `idx_group` (`group_id`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天消息表';
-- 2. 群组表
CREATE TABLE `chat_group` (
`id` VARCHAR(64) NOT NULL COMMENT '群组ID(UUID无横线)',
`group_name` VARCHAR(128) DEFAULT NULL COMMENT '群组名称',
`group_type` TINYINT DEFAULT 1 COMMENT '群组类型: 1=固定群, 2=临时群',
`owner_id` VARCHAR(64) DEFAULT NULL COMMENT '群主用户ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天群组表';
-- 3. 群组成员表
CREATE TABLE `chat_group_member` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '成员记录ID',
`group_id` VARCHAR(64) NOT NULL COMMENT '群组ID',
`user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
`nickname` VARCHAR(128) DEFAULT NULL COMMENT '群内昵称',
`role` TINYINT DEFAULT 0 COMMENT '角色: 0=普通成员, 1=管理员, 2=群主',
`join_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_user` (`group_id`, `user_id`),
INDEX `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='群组成员表';
-- 4. 会话表
CREATE TABLE `chat_conversation` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '会话记录ID',
`user_id` VARCHAR(64) NOT NULL COMMENT '用户ID(会话归属)',
`conversation_type` TINYINT NOT NULL COMMENT '会话类型: 1=私聊, 2=群聊, 3=系统通知',
`target_id` VARCHAR(64) NOT NULL COMMENT '目标ID',
`last_message_id` BIGINT DEFAULT NULL COMMENT '最新消息ID',
`last_message_time` DATETIME DEFAULT NULL COMMENT '最新消息时间',
`unread_count` INT DEFAULT 0 COMMENT '未读消息数',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_conversation` (`user_id`, `conversation_type`, `target_id`),
INDEX `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天会话表';
三、后端架构设计
3.1 包结构
cn.hsa.spp.chat/
├── config/
│ ├── WebSocketConfig.java # WebSocket 注册配置
│ └── ChatHandshakeInterceptor.java # WebSocket 握手认证拦截器
├── controller/
│ └── ChatController.java # REST API 控制器
├── dto/
│ └── WebSocketMessage.java # WebSocket 消息通用包装
├── entity/
│ ├── ChatMessage.java # 消息实体
│ ├── ChatConversation.java # 会话实体
│ ├── ChatGroup.java # 群组实体
│ └── ChatGroupMember.java # 群组成员实体
├── handler/
│ └── ChatWebSocketHandler.java # WebSocket 消息处理器
├── mapper/
│ ├── ChatMessageMapper.java
│ ├── ChatConversationMapper.java
│ ├── ChatGroupMapper.java
│ └── ChatGroupMemberMapper.java
└── service/
├── ChatService.java / impl/
└── GroupService.java / impl/
3.2 认证机制
握手认证流程:
客户端连接: ws://host:port/{context-path}/ws/chat?token={jwt_token}
服务端 ChatHandshakeInterceptor.beforeHandshake():
1. 从 URL 参数提取 token
2. 用 JwtUtil.decode(token) 解析 JWT 获取 user-key
3. 从 Redis 查询 auth:userId:{user-key} 获取用户信息
4. 验证通过后,将 userId 存入 WebSocketSession.attributes
5. 握手成功,返回 true
迁移适配: 如果目标系统的 JWT 或 Redis 结构不同,只需修改 ChatHandshakeInterceptor 中的解析逻辑。
3.3 WebSocket 消息协议
消息格式(统一包装)
{
"type": "MESSAGE_TYPE",
"data": { ... },
"messageId": 12345,
"sendTime": "2026-07-22 10:00:00"
}
消息类型
| 客户端→服务端 |
说明 |
data 字段 |
| PING |
心跳检测 |
{ "token": "..." } |
| PRIVATE_MESSAGE |
发送私聊消息 |
{ "receiverId": "...", "content": "...", "contentType": 1 } |
| GROUP_MESSAGE |
发送群聊消息 |
{ "groupId": "...", "content": "...", "contentType": 1 } |
| 服务端→客户端 |
说明 |
data 字段 |
| CONNECT |
连接成功 |
"连接成功" |
| PONG |
心跳响应 |
"pong" |
| PRIVATE_MESSAGE |
推送私聊消息 |
{ "id": 123, "senderId": "...", "content": "...", "createTime": "..." } |
| GROUP_MESSAGE |
推送群聊消息 |
{ "id": 123, "senderId": "...", "groupId": "...", "content": "...", "createTime": "..." } |
| SYSTEM_NOTICE |
系统通知 |
{ "id": 123, "title": "...", "content": "...", "createTime": "..." } |
3.4 REST API 设计
基础路径:/web/mcsTrade/chat(迁移时按目标系统调整)
| 方法 |
路径 |
说明 |
| GET |
/history/private |
私聊历史:userId1, userId2, page, size |
| GET |
/history/group/{groupId} |
群聊历史:page, size |
| GET |
/conversations |
会话列表:userId |
| PUT |
/conversations/{id}/read |
标记会话已读 |
| GET |
/groups |
用户所在群组列表:userId |
| POST |
/groups |
创建群组:groupName, ownerId |
| GET |
/groups/{groupId}/members |
群组成员列表 |
| POST |
/groups/{groupId}/join |
加入群组:userId |
| POST |
/groups/{groupId}/quit |
退出群组:userId |
| POST |
/notices/send |
发送系统通知:userId, title, content |
四、前端架构设计
4.1 目录结构
src/
├── api/
│ └── chat.js # 聊天 API 调用封装
├── utils/
│ └── chatSocket.js # WebSocket 客户端封装类
└── views/
└── chat/
├── ChatLayout.vue # 聊天主布局
├── ConversationList.vue # 会话列表组件
├── PrivateChat.vue # 私聊窗口
├── GroupChat.vue # 群聊窗口
├── GroupMembersDialog.vue # 群成员管理弹窗
└── SystemNotice.vue # 系统通知展示
4.2 ChatSocket 客户端类设计
class ChatSocket {
constructor(options) {
this.url = options.url;
this.token = options.token;
this.userId = options.userId;
this.handlers = {};
this.reconnectInterval = options.reconnectInterval || 3000;
this.heartbeatInterval = options.heartbeatInterval || 30000;
}
connect() // 建立 WebSocket 连接
disconnect() // 关闭连接
send(type, data) // 发送消息
on(type, callback) // 注册消息处理回调
off(type) // 注销消息处理回调
isConnected() // 连接状态
// 内部方法
_startHeartbeat() // 启动心跳
_handleMessage(event) // 处理收到的消息
_reconnect() // 自动重连
}
五、关键实现代码
5.1 WebSocket 配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatWebSocketHandler(), "/ws/chat")
.addInterceptors(chatHandshakeInterceptor())
.setAllowedOrigins("*");
}
@Bean
public ChatWebSocketHandler chatWebSocketHandler() {
return new ChatWebSocketHandler();
}
@Bean
public ChatHandshakeInterceptor chatHandshakeInterceptor() {
return new ChatHandshakeInterceptor();
}
}
5.2 握手认证拦截器
public class ChatHandshakeInterceptor implements HandshakeInterceptor {
@Autowired
private HsafRedisTemplate hsafRedisTemplate;
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
String query = request.getURI().getQuery();
Map<String, String> params = parseQuery(query);
String token = params.get("token");
if (StringUtils.isEmpty(token)) {
return false;
}
try {
String userKey = jwtUtil.getUserCode(token);
String userJson = hsafRedisTemplate.opsForValue().get("auth:userId:" + userKey);
if (userJson == null) {
return false;
}
JSONObject user = JSONObject.parseObject(userJson);
attributes.put("userId", user.getString("userId"));
return true;
} catch (Exception e) {
return false;
}
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {}
}
5.3 WebSocket 消息处理器核心逻辑
public class ChatWebSocketHandler extends TextWebSocketHandler {
private final Map<String, WebSocketSession> userSessions = new ConcurrentHashMap<>();
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
WebSocketMessage wsMsg = JSON.parseObject(message.getPayload(), WebSocketMessage.class);
String type = wsMsg.getType();
JSONObject data = wsMsg.getData();
if ("PING".equals(type)) {
session.send(new TextMessage(JSON.toJSONString(
new WebSocketMessage("PONG", null, null, now()))));
return;
}
String userId = getUserId(session);
if (userId == null) {
session.close(CloseStatus.POLICY_VIOLATION);
return;
}
switch (type) {
case "PRIVATE_MESSAGE":
chatService.sendPrivateMessage(userId, data);
break;
case "GROUP_MESSAGE":
chatService.sendGroupMessage(userId, data);
break;
}
}
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String userId = getUserId(session);
if (userId != null) {
userSessions.put(userId, session);
session.send(new TextMessage(JSON.toJSONString(
new WebSocketMessage("CONNECT", "连接成功", null, now()))));
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String userId = getUserId(session);
if (userId != null) {
userSessions.remove(userId);
}
}
// 发送给指定用户
public void sendToUser(String userId, WebSocketMessage message) {
WebSocketSession session = userSessions.get(userId);
if (session != null && session.isOpen()) {
session.send(new TextMessage(JSON.toJSONString(message)));
}
}
}
六、水平扩展方案
6.1 当前限制
当前实现使用 ConcurrentHashMap<String, WebSocketSession> 管理会话,仅适用于单实例部署。
6.2 Redis Pub/Sub 扩展方案
多实例部署时,需要引入 Redis Pub/Sub 机制:
- 引入 Redis Pub/Sub 依赖(项目已有
spring-boot-starter-data-redis)
- 新增
ChatRedisPublisher:发送消息到 Redis channel
- 新增
ChatRedisSubscriber:订阅 Redis channel,收到消息后通过本地 WS Map 推送
- 发送群消息/跨节点消息时,先持久化到 MySQL,再发布到 Redis
- 所有节点订阅同一个 channel,实现跨实例消息路由
七、迁移检查清单
7.1 数据库
7.2 后端依赖
7.3 代码迁移
7.4 配置适配
7.5 前端迁移
7.6 功能验证
八、文件清单(按依赖顺序)
| 序号 |
文件路径 |
说明 |
| 1 |
sql/chat/chat_tables.sql |
数据库建表脚本 |
| 2 |
entity/ChatMessage.java |
消息实体 |
| 3 |
entity/ChatConversation.java |
会话实体 |
| 4 |
entity/ChatGroup.java |
群组实体 |
| 5 |
entity/ChatGroupMember.java |
群组成员实体 |
| 6 |
dto/WebSocketMessage.java |
WebSocket 消息包装 DTO |
| 7 |
mapper/ChatMessageMapper.java |
消息 Mapper 接口 |
| 8 |
mapper/ChatConversationMapper.java |
会话 Mapper 接口 |
| 9 |
mapper/ChatGroupMapper.java |
群组 Mapper 接口 |
| 10 |
mapper/ChatGroupMemberMapper.java |
群组成员 Mapper 接口 |
| 11 |
sql/chat/ChatMessageMapper.xml |
消息 Mapper XML |
| 12 |
sql/chat/ChatConversationMapper.xml |
会话 Mapper XML |
| 13 |
sql/chat/ChatGroupMemberMapper.xml |
群组成员 Mapper XML |
| 14 |
config/ChatHandshakeInterceptor.java |
握手认证拦截器 |
| 15 |
config/WebSocketConfig.java |
WebSocket 配置 |
| 16 |
handler/ChatWebSocketHandler.java |
WebSocket 消息处理器 |
| 17 |
service/ChatService.java |
聊天业务接口 |
| 18 |
service/impl/ChatServiceImpl.java |
聊天业务实现 |
| 19 |
service/GroupService.java |
群组业务接口 |
| 20 |
service/impl/GroupServiceImpl.java |
群组业务实现 |
| 21 |
controller/ChatController.java |
REST 控制器 |
| 22 |
src/api/chat.js |
前端 API 封装 |
| 23 |
src/utils/chatSocket.js |
前端 WebSocket 客户端 |
| 24 |
src/views/chat/ChatLayout.vue |
聊天主布局 |
| 25 |
src/views/chat/ConversationList.vue |
会话列表组件 |
| 26 |
src/views/chat/PrivateChat.vue |
私聊窗口 |
| 27 |
src/views/chat/GroupChat.vue |
群聊窗口 |
| 28 |
src/views/chat/GroupMembersDialog.vue |
群成员弹窗 |
| 29 |
src/views/chat/SystemNotice.vue |
系统通知组件 |
九、可选扩展功能
| 功能 |
说明 |
优先级 |
| 消息已读回执 |
发送方确认对方已读 |
低 |
| 消息撤回 |
用户撤回发送的消息 |
低 |
| 离线消息推送 |
用户上线后推送离线期间的消息 |
中 |
| 消息加密 |
对 content 字段加密存储传输 |
中 |
| 敏感词过滤 |
消息内容敏感词检测过滤 |
低 |