websocket实现系统聊天功能

聊天系统迁移设计方案

一、系统概览

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

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 消息协议

消息格式(统一包装)
json 复制代码
{
  "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 客户端类设计

javascript 复制代码
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 配置

java 复制代码
@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 握手认证拦截器

java 复制代码
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 消息处理器核心逻辑

java 复制代码
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 机制:

  1. 引入 Redis Pub/Sub 依赖(项目已有 spring-boot-starter-data-redis
  2. 新增 ChatRedisPublisher:发送消息到 Redis channel
  3. 新增 ChatRedisSubscriber:订阅 Redis channel,收到消息后通过本地 WS Map 推送
  4. 发送群消息/跨节点消息时,先持久化到 MySQL,再发布到 Redis
  5. 所有节点订阅同一个 channel,实现跨实例消息路由

七、迁移检查清单

7.1 数据库

  • 在目标库执行 chat_tables.sql
  • 确认 MyBatis mapper-locations 配置包含 classpath*:sql/*/*.xml
  • 确认数据库连接配置正确

7.2 后端依赖

  • 添加 spring-boot-starter-websocket 依赖
  • 确认 Redis 连接配置可用
  • 确认 JWT 解析工具可用(JwtUtil 或等价实现)
  • 确认 MyBatis-Plus 配置正确

7.3 代码迁移

  • 迁移 config/ 目录(WebSocket 配置 + 握手拦截器)
  • 迁移 entity/ 目录(4个实体类)
  • 迁移 dto/ 目录(WebSocketMessage)
  • 迁移 handler/ 目录(ChatWebSocketHandler)
  • 迁移 mapper/ 目录(4个 Mapper 接口)
  • 迁移 sql/chat/*.xml(Mapper XML 文件)
  • 迁移 service/ 目录(业务逻辑)
  • 迁移 controller/ChatController.java
  • 修改包名(cn.hsa.spp.chat → 目标系统包名)

7.4 配置适配

  • 适配 JwtUtil.getUserCode(token) 的 JWT 结构
  • 适配 Redis key 前缀(auth:userId: → 目标系统格式)
  • 适配 REST API 路径前缀
  • 适配 WebSocket 路径(/ws/chat
  • 适配上下文路径

7.5 前端迁移

  • 迁移 api/chat.js
  • 迁移 utils/chatSocket.js
  • 迁移 views/chat/ 下所有 Vue 组件
  • 适配 API 请求路径
  • 适配 WebSocket 连接 URL

7.6 功能验证

  • WebSocket 连接成功
  • 私聊消息发送/接收
  • 群聊消息发送/接收
  • 系统通知推送
  • 消息历史查询
  • 会话列表展示
  • 未读数更新
  • 重连机制正常

八、文件清单(按依赖顺序)

序号 文件路径 说明
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 字段加密存储传输
敏感词过滤 消息内容敏感词检测过滤
相关推荐
GitLqr2 天前
玩转 WebSocket:从原理到 Flutter 实战
websocket·网络协议·flutter
乱七八糟的屋子3 天前
Poco C++高级实战教程:线程池+日志系统+加密算法+WebSocket长连接
c++·websocket·加密解密·#poco·#c++高级开发
宠友信息5 天前
MySQL复合索引与Druid优化仿小红书源码个人主页查询链路
数据库·spring boot·websocket·mysql·uni-app
程序猿乐锅5 天前
【苍穹外卖 day10|Spring Task、WebSocket 与来单提醒、催单铃声实现】
java·websocket·spring
宠友信息6 天前
消息撤回与已读状态如何在即时通讯源码中统一管理
java·spring boot·websocket·mysql·uni-app
paopaokaka_luck8 天前
基于Springboot3+vue3的旅游景区点评系统(AI审核、webSocket聊天、协同过滤算法、Echarts图形化分析)
websocket·网络协议·旅游
熬夜苦读学习8 天前
基于websocket的多用户五子棋网页游戏
linux·服务器·网络·websocket·网络协议·游戏·五子棋
米尔的可达鸭8 天前
深入操作系统 Socket 底层:EPOLLOUT 可写事件管理 + 非阻塞异步
开发语言·网络·数据结构·经验分享·websocket·网络协议
米尔的可达鸭9 天前
深入操作系统 Socket 底层:套接字控制块、FD映射、阻塞IO核心完整实现
arm开发·数据结构·websocket·网络协议·算法·架构·安全架构