SpringBoot 即时聊天 IM 完整实现(HTTP会话管理 + WebSocket实时推送 + 离线消息)
前言近期基于若依(RuoYi-Vue)后台框架自研一套站内即时聊天模块完整覆盖会话创建、会话列表、历史消息、未读标记、实时消息推送、离线消息补发、心跳保活、Token鉴权等核心能力。整体技术架构分层清晰ControllerHTTP 接口提供前端会话、历史消息、已读标记等查询操作Service核心业务逻辑会话防重复创建、消息入库、未读数统计、离线消息处理Mapper MyBatis复杂联表查询实时计算用户专属未读消息、关联用户昵称头像WebSocket长连接实时推送、JWT鉴权、心跳PING/PONG、在线用户管理、离线消息推送MySQL数据表chat_session会话表 chat_message消息表配套唯一索引、联合查询索引解决并发与查询性能问题本文完整贴出建表SQL、实体DTO、Controller、Service、MapperXML、WebSocket全套源码并附带设计思路、踩坑解决方案可直接集成到现有SpringBoot后台项目。二、整体设计思路会话唯一设计解决重复创建会话并发问题两人聊天仅存在唯一一条会话记录规则不区分发起人与接收人统一 minId小用户ID、maxId大用户ID 存储数据库对 from_user_id, to_user_id 建立联合唯一索引杜绝重复会话并发同时创建会话时捕获唯一索引冲突二次查询返回已有会话避免重复数据未读数实时计算方案放弃会话表冗余未读字段单一计数采用实时子查询统计联表 chat_message 统计当前登录用户所有未读消息支持多端登录、精准区分“发给我的未读”避免不同用户查看同一会话未读数错乱事务与解耦优化消息入库、会话更新、未读数自增放入事务WebSocket消息推送放到事务外部执行防止长连接推送阻塞数据库事务持有推送失败不影响消息持久化用户可通过会话列表轮询拉取离线消息兜底WebSocket 安全设计握手阶段校验URL路径userId URL携带JWT Token双重校验本地解析JWT密钥和后台登录体系统一校验过期、非法Token直接关闭连接单用户仅保留一条在线连接新连接自动剔除旧连接PING/PONG心跳保活处理前端断线重连场景发送消息加同步锁规避多线程并发发送报通道异常离线消息补发用户WebSocket上线时自动查询所有未读消息逐条推送推送完成批量标记已读避免重复推送。三、MySQL 数据库建表语句3.1 chat_session 聊天会话表存储两个用户一对一会话基础信息联合唯一索引保证双人仅一条会话/*Navicat Premium Data TransferSource Server : 46Source Server Type : MySQLSource Server Version : 80045Source Host : 192.168.1.46:3306Source Schema : shan_guo_devTarget Server Type : MySQLTarget Server Version : 80045File Encoding : 65001Date: 10/07/2026 09:45:30*/SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS 0;– Table structure for chat_sessionDROP TABLE IF EXISTSchat_session;CREATE TABLEchat_session(idbigint NOT NULL AUTO_INCREMENT COMMENT ‘会话主键ID’,from_user_idbigint NOT NULL COMMENT ‘小ID用户’,to_user_idbigint NOT NULL COMMENT ‘大ID用户’,last_contentvarchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT ‘会话最后一条消息内容(列表预览)’,last_msg_timedatetime NULL DEFAULT NULL COMMENT ‘会话最后一条消息发送时间(排序依据)’,unread_countint NULL DEFAULT 0 COMMENT ‘当前会话未读消息数量(冗余字段)’,create_timedatetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘会话创建时间’,update_timedatetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ‘会话更新时间’,PRIMARY KEY (id) USING BTREE,– 双人会话唯一约束防止重复创建会话UNIQUE INDEXuk_user_pair(from_user_id,to_user_id) USING BTREE,INDEXidx_from(from_user_id) USING BTREE,INDEXidx_to(to_user_id) USING BTREE) ENGINE InnoDB AUTO_INCREMENT 3 CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT ‘聊天会话表’ ROW_FORMAT Dynamic;SET FOREIGN_KEY_CHECKS 1;优化说明原SQL存在重复唯一索引 uk_from_to、uk_user_pair删除重复索引仅保留 uk_user_pair减少索引占用。3.2 chat_message 聊天消息表存储每条聊天记录联合索引加速未读消息查询/*Navicat Premium Data TransferSource Server : 46Source Server Type : MySQLSource Server Version : 80045Source Host : 192.168.1.46:3306Source Schema : shan_guo_devTarget Server Type : MySQLTarget Server Version : 80045File Encoding : 65001Date: 10/07/2026 09:45:38*/SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS 0;– Table structure for chat_messageDROP TABLE IF EXISTSchat_message;CREATE TABLEchat_message(idbigint NOT NULL AUTO_INCREMENT COMMENT ‘消息主键ID’,session_idbigint NOT NULL COMMENT ‘所属会话ID’,sender_idbigint NOT NULL COMMENT ‘消息发送人用户ID’,receiver_idbigint NOT NULL COMMENT ‘消息接收人用户ID’,contentvarchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT ‘消息内容’,msg_typechar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT ‘1’ COMMENT ‘消息类型1-文本 2-图片’,read_statuschar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT ‘0’ COMMENT ‘读取状态0-未读 1-已读’,create_timedatetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘消息发送时间’,PRIMARY KEY (id) USING BTREE,– 根据会话查询历史消息索引INDEXidx_session(session_id) USING BTREE,– 用户未读消息查询联合索引优化离线消息加载INDEXidx_receiver_read(receiver_id,read_status) USING BTREE) ENGINE InnoDB AUTO_INCREMENT 11 CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT ‘聊天消息表’ ROW_FORMAT Dynamic;SET FOREIGN_KEY_CHECKS 1;四、实体类 DTO 完整代码4.1 ChatSession 会话实体package com.para.system.chat.domain;import lombok.Data;import java.util.Date;/**聊天会话实体/Datapublic class ChatSession {/* 会话主键/private Long id;/* 较小用户ID/private Long fromUserId;/* 较大用户ID/private Long toUserId;/* 最后一条消息内容/private String lastContent;/* 最后消息时间/private Date lastMsgTime;/* 冗余未读数/private Integer unreadCount;/* 当前用户实时未读数查询计算字段/private Integer realSelfUnread;/* 创建时间/private Date createTime;/* 更新时间 */private Date updateTime;// 联表sys_user查询字段/** 对方昵称/private String targetNickName;/* 对方头像 */private String targetAvatar;}4.2 ChatMessage 消息实体package com.para.system.chat.domain;import lombok.Data;import java.util.Date;/**聊天消息实体/Datapublic class ChatMessage {/* 消息主键/private Long id;/* 会话ID/private Long sessionId;/* 发送人ID/private Long senderId;/* 接收人ID/private Long receiverId;/* 消息内容/private String content;/* 消息类型 1文本 2图片/private String msgType;/* 读取状态 0未读 1已读/private String readStatus;/* 发送时间 */private Date createTime;}4.3 ChatMsgDTO WebSocket传输DTO前后端WebSocket交互统一载体package com.para.system.chat.domain;import lombok.Data;/**WebSocket消息传输DTO/Datapublic class ChatMsgDTO {/* 会话ID/private Long sessionId;/* 接收人ID/private Long receiverId;/* 消息内容/private String content;/* 消息类型 1文本 2图片 */private String msgType;}4.4 ChatService 顶层接口package com.para.system.chat.service;import com.para.system.chat.domain.ChatMessage;import com.para.system.chat.domain.ChatSession;import com.para.system.chat.domain.ChatMsgDTO;import java.util.List;public interface ChatService {/** * 获取或创建双人会话 */ ChatSession getOrCreateSession(Long fromUid, Long toUid); /** * 保存消息并推送 */ void saveAndPushMessage(Long senderId, ChatMsgDTO dto); /** * 查询会话历史消息 */ ListChatMessage history(Long sessionId); /** * 查询用户会话列表支持昵称搜索 */ ListChatSession sessionList(Long uid, String nickName); /** * 批量标记会话全部消息已读 */ void markAllRead(Long sessionId, Long uid); /** * 用户上线推送离线未读消息 */ void pushOfflineMessage(Long uid);}五、代码分层实现5.1 Controller 层ChatController HTTP接口提供前端所有同步查询接口创建会话、会话列表支持昵称搜索、历史消息分页、标记消息已读。统一继承项目基础控制器内置分页工具、登录用户工具类。package com.para.system.chat.controller;import com.para.common.core.controller.BaseController;import com.para.common.core.domain.AjaxResult;import com.para.common.core.page.TableDataInfo;import com.para.common.utils.SecurityUtils;import com.para.system.chat.domain.ChatSession;import com.para.system.chat.service.ChatService;import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;import java.util.List;/**聊天接口控制器提供前端HTTP接口会话管理、消息查询、已读标记*/RestControllerRequestMapping(“/chat”)public class ChatController extends BaseController {Resourceprivate ChatService chatService;/**获取或创建与对方的会话param toUserId 对方用户ID*/GetMapping(“/session”)public AjaxResult session(RequestParam Long toUserId) {try {Long userId SecurityUtils.getLoginUser().getUserId();ChatSession session chatService.getOrCreateSession(userId, toUserId);return AjaxResult.success(session);} catch (RuntimeException e) {return AjaxResult.error(e.getMessage());}}/**获取当前用户的会话列表按最后消息时间倒序支持对方昵称模糊搜索*/GetMapping(“/sessionList”)public TableDataInfo sessionList(RequestParam(required false) String nickName) {startPage();Long userId SecurityUtils.getLoginUser().getUserId();List list chatService.sessionList(userId, nickName);return getDataTable(list);}/**获取会话历史消息分页*/GetMapping(“/history”)public TableDataInfo history(RequestParam Long sessionId) {startPage();return getDataTable(chatService.history(sessionId));}/**标记会话所有消息为已读清空未读计数*/PostMapping(“/read”)public AjaxResult read(RequestParam Long sessionId) {Long userId SecurityUtils.getLoginUser().getUserId();chatService.markAllRead(sessionId, userId);return AjaxResult.success();}