SpringBoot + WebSocket + Layui + Vue 构建聊天交流模块详细设计与实现
一、概述
本文将介绍一种基于 SpringBoot、WebSocket、layui、vue 的聊天交流模块的详细设计与实现。该模块的主要功能是实现在线聊天,支持群聊和私聊,同时提供聊天记录的保存和查询功能。
二、系统架构
- 前端页面
前端页面采用 layui 和 vue 框架,通过 WebSocket 与后端进行通信。
- 后端服务
后端服务采用 SpringBoot 框架,并通过 WebSocket 处理客户端的聊天请求。同时,通过 Mybatis 对数据库进行操作,实现聊天记录的保存和查询功能。
三、模块设计
- 数据库设计
本模块使用 MySQL 数据库,设计了以下两张表:
1)user 表:保存用户信息,包括用户 ID、用户名、密码等字段。
2)chat_record 表:保存聊天记录,包括发送者 ID、接收者 ID、消息内容、发送时间等字段。
- 后端服务设计
1)WebSocket 处理
后端服务使用 WebSocket 处理客户端的聊天请求。通过 @ServerEndpoint 注解标注一个 WebSocket 处理器类,接收客户端的连接请求。
2)消息处理
后端服务通过 @OnMessage 注解标注一个方法,接收客户端发送的消息,并进行处理。根据消息类型,将消息转发给指定的用户或群组。
3)持久化
后端服务使用 Mybatis 进行数据库操作,将聊天记录保存到 chat_record 表中,并提供查询接口,供客户端查询历史聊天记录。
- 前端页面设计
1)登录页面
用户在登录页面输入用户名和密码,验证通过后进入聊天界面。
2)聊天界面
聊天界面分为左右两个区域,左侧为用户列表,右侧为聊天窗口。用户列表中显示当前在线的用户和群组,用户可以选择私聊或群聊。聊天窗口中显示当前聊天的内容,用户可以发送消息、表情和图片。
四、模块实现
- 数据库实现
使用 MySQL 数据库,创建 user 表和 chat_record 表,并插入测试数据。
- 后端服务实现
1)WebSocket 处理
使用 @ServerEndpoint 注解标注 ChatEndpoint 类,处理 WebSocket 连接请求。
@ServerEndpoint("/chat")
@Component
public class ChatEndpoint {
private static final Logger logger = LoggerFactory.getLogger(ChatEndpoint.class);
private static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
@Autowired
private UserService userService;
@Autowired
private ChatRecordService chatRecordService;
/**
* 处理连接请求
*/
@OnOpen
public void onOpen(Session session) {
logger.info('WebSocket 连接成功,sessionID={}', session.getId());
String userId = getUserId(session);
if (userId != null) {
sessionMap.put(userId, session);
sendMessage(session, new Message(MessageType.SYSTEM, '连接成功'));
} else {
logger.error('用户 ID 为空');
sendMessage(session, new Message(MessageType.SYSTEM, '连接失败'));
}
}
/**
* 处理断开连接请求
*/
@OnClose
public void onClose(Session session, CloseReason closeReason) {
logger.info('WebSocket 连接关闭,sessionID={}', session.getId());
String userId = getUserId(session);
if (userId != null) {
sessionMap.remove(userId);
}
}
/**
* 处理消息请求
*/
@OnMessage
public void onMessage(Session session, String message) {
logger.info('接收到消息,message={}', message);
String userId = getUserId(session);
if (userId != null) {
Message msg = JsonUtils.fromJson(message, Message.class);
if (msg.getType() == MessageType.CHAT) {
handleChatMessage(userId, msg);
} else if (msg.getType() == MessageType.PING) {
handlePingMessage(session);
}
} else {
logger.error('用户 ID 为空');
}
}
/**
* 处理错误请求
*/
@OnError
public void onError(Session session, Throwable throwable) {
logger.error('WebSocket 异常,sessionID={}', session.getId(), throwable);
}
/**
* 处理聊天消息
*/
private void handleChatMessage(String userId, Message message) {
String toUserId = message.getToUserId();
String content = message.getContent();
String sendTime = message.getSendTime();
if (StringUtils.isEmpty(toUserId) || StringUtils.isEmpty(content) || StringUtils.isEmpty(sendTime)) {
logger.error('聊天消息格式不正确,message={}', message);
return;
}
// 保存聊天记录
ChatRecord chatRecord = new ChatRecord();
chatRecord.setSenderId(userId);
chatRecord.setReceiverId(toUserId);
chatRecord.setContent(content);
chatRecord.setSendTime(sendTime);
chatRecordService.insert(chatRecord);
// 转发消息
if (toUserId.equals('all')) {
broadcastMessage(userId, message);
} else {
sendMessage(toUserId, message);
}
}
/**
* 处理心跳消息
*/
private void handlePingMessage(Session session) {
sendMessage(session, new Message(MessageType.PONG));
}
/**
* 广播消息
*/
private void broadcastMessage(String userId, Message message) {
sessionMap.forEach((id, session) -> {
if (!userId.equals(id)) {
sendMessage(session, message);
}
});
}
/**
* 发送消息给指定用户
*/
private void sendMessage(String userId, Message message) {
Session session = sessionMap.get(userId);
if (session != null && session.isOpen()) {
sendMessage(session, message);
} else {
logger.error('用户未连接,userId={}', userId);
}
}
/**
* 发送系统消息
*/
private void sendSystemMessage(String userId, String content) {
sendMessage(userId, new Message(MessageType.SYSTEM, content));
}
/**
* 发送消息给指定 session
*/
private void sendMessage(Session session, Message message) {
try {
String json = JsonUtils.toJson(message);
session.getBasicRemote().sendText(json);
} catch (IOException e) {
logger.error('发送消息失败', e);
}
}
/**
* 获取用户 ID
*/
private String getUserId(Session session) {
String userId = (String) session.getUserProperties().get('userId');
if (StringUtils.isEmpty(userId)) {
String token = session.getRequestParameterMap().get('token').get(0);
userId = JwtUtils.getUserId(token);
if (StringUtils.isNotEmpty(userId)) {
session.getUserProperties().put('userId', userId);
}
}
return userId;
}
}
2)消息处理
根据消息类型,将消息转发给指定的用户或群组。
3)持久化
使用 Mybatis 进行数据库操作,保存聊天记录到 chat_record 表中,并提供查询接口。
@Service
public class ChatRecordServiceImpl implements ChatRecordService {
@Autowired
private ChatRecordMapper chatRecordMapper;
@Override
public void insert(ChatRecord chatRecord) {
chatRecordMapper.insert(chatRecord);
}
@Override
public List<ChatRecord> list(String senderId, String receiverId) {
return chatRecordMapper.list(senderId, receiverId);
}
}
- 前端页面实现
1)登录页面
用户在登录页面输入用户名和密码,通过 Ajax 请求后端服务进行验证。
<body>
<div class="container">
<form class="layui-form" action="">
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="username" required lay-verify="required" placeholder="请输入用户名" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="password" name="password" required lay-verify="required" placeholder="请输入密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="loginBtn">登录</button>
</div>
</div>
</form>
</div>
<script src="/js/layui/layui.js"></script>
<script src="/js/jquery/jquery-3.5.1.min.js"></script>
<script>
layui.use(['form', 'layer'], function() {
var form = layui.form;
var layer = layui.layer;
form.on('submit(loginBtn)', function(data) {
$.ajax({
type: 'POST',
url: '/chat/login',
dataType: 'json',
data: data.field,
success: function(res) {
if (res.code == 0) {
window.location.href = '/chat';
} else {
layer.msg(res.msg, {icon: 5});
}
},
error: function() {
layer.msg('登录失败', {icon: 5});
}
});
return false;
});
});
</script>
</body>
2)聊天界面
聊天界面分为左右两个区域,左侧为用户列表,右侧为聊天窗口。用户列表中显示当前在线的用户和群组,用户可以选择私聊或群聊。聊天窗口中显示当前聊天的内容,用户可以发送消息、表情和图片。
<body>
<div id="userList" class="layui-collapse layui-hide">
<div class="layui-colla-item">
<h2 class="layui-colla-title">在线用户</h2>
<div class="layui-colla-content layui-show">
<ul id="userListOnline" class="layui-nav layui-nav-tree layui-nav-side"></ul>
</div>
</div>
<div class="layui-colla-item">
<h2 class="layui-colla-title">群组</h2>
<div class="layui-colla-content layui-show">
<ul id="groupList" class="layui-nav layui-nav-tree layui-nav-side"></ul>
</div>
</div>
</div>
<div id="chatBox" class="layui-hide">
<div class="layui-row">
<div class="layui-col-md3">
<ul id="chatList" class="layui-nav layui-nav-tree layui-nav-side"></ul>
</div>
<div class="layui-col-md9">
<div id="chatContent" class="layui-card-body"></div>
<div id="chatInput" class="layui-card-body">
<form class="layui-form" action="">
<div class="layui-form-item">
<div class="layui-input-block">
<textarea id="msgContent" name="content" required lay-verify="required" placeholder="请输入消息内容" autocomplete="off" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button id="sendBtn" class="layui-btn" lay-submit lay-filter="sendMessage">发送</button>
<button id="emojiBtn" class="layui-btn layui-btn-primary">表情</button>
<button id="imageBtn" class="layui-btn layui-btn-primary">图片</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="emojiBox" class="layui-hide">
<ul id="emojiList" class="layui-nav layui-nav-tree layui-nav-side"></ul>
</div>
<div id="imageBox" class="layui-hide">
<div class="layui-upload">
<button class="layui-btn layui-btn-normal layui-btn-sm" id="uploadBtn"><i class="layui-icon"></i>上传图片</button>
<div class="layui-upload-list">
<img id="previewImage" src="" style="max-width: 100px;">
<p id="previewText"></p>
</div>
</div>
</div>
<script src="/js/layui/layui.js"></script>
<script src="/js/jquery/jquery-3.5.1.min.js"></script>
<script>
layui.use(['element', 'form', 'layim', 'laytpl', 'layer', 'upload'], function() {
var element = layui.element;
var form = layui.form;
var layim = layui.layim;
var laytpl = layui.laytpl;
var layer = layui.layer;
var upload = layui.upload;
var token = localStorage.getItem('token');
if (token == null) {
window.location.href = '/login.html';
return;
}
// 初始化 WebSocket 连接
var socket = new WebSocket("ws://" + window.location.host + "/chat?token=" + token);
socket.onopen = function(event) {
console.log("WebSocket 连接成功");
};
socket.onmessage = function(event) {
var message = JSON.parse(event.data);
console.log("接收到消息:", message);
if (message.type == 1) {
// 系统消息
layer.msg(message.content);
} else if (message.type == 2) {
// 聊天消息
if (message.fromUserId == layim.cache().mine.id) {
// 自己发送的消息
appendChatMessage(message.toUserId, message);
} else if (message.toUserId == layim.cache().mine.id) {
// 收到的消息
appendChatMessage(message.fromUserId, message);
}
}
};
socket.onclose = function(event) {
console.log("WebSocket 连接关闭");
};
socket.onerror = function(event) {
console.error("WebSocket 连接异常", event);
};
// 表情列表
var emojiList = [
{"title": "默认", "data": [
{"alt": '[可爱]', "src": "/images/emoji/1.png"},
{"alt": '[开心]', "src": "/images/emoji/2.png"},
{"alt": '[大笑]', "src": "/images/emoji/3.png"},
{"alt": '[白眼]', "src": "/images/emoji/4.png"},
{"alt": '[抠鼻]', "src": "/images/emoji/5.png"},
{"alt": '[惊讶]', "src": "/images/emoji/6.png"},
{"alt": '[委屈]', "src": "/images/emoji/7.png"},
{"alt": '[流泪]', "src": "/images/emoji/8.png"},
{"alt": '[花心]', "src": "/images/emoji/9.png"},
{"alt": '[心碎]', "src": "/images/emoji/10.png"},
{"alt": '[生气]', "src": "/images/emoji/11.png"},
{"alt": '[想吃]', "src": "/images/emoji/12.png"},
{"alt": '[困]', "src": "/images/emoji/13.png"},
{"alt": '[睡觉]', "src": "/images/emoji/14.png"},
{"alt": '[打哈欠]', "src": "/images/emoji/15.png"},
{"alt": '[好困]', "src": "/images/emoji/16.png"},
{"alt": '[好困]', "src": "/images/emoji/16.png'}
]}
];
// 表情弹窗
laytpl('{{# for(var i=0; i<emojiList.length; i++){ }}' +
'<li class="layui-nav-item">' +
'<a href="javascript:void(0);" class="layui-nav-item">{{ emojiList[i].title }}</a>' +
'<dl class="layui-nav-child">' +
'{{# for(var j=0; j<emojiList[i].data.length; j++){ }}' +
'<dd><img src="{{ emojiList[i].data[j].src }}" alt="{{ emojiList[i].data[j].alt }}" title="{{ emojiList[i].data[j].alt }}" class="layui-emoji"></dd>' +
'{{# } }}' +
'</dl>' +
'</li>' +
'{{# } }}').render({emojiList: emojiList}, function(html) {
$("#emojiList").html(html);
$("#emojiList img").click(function() {
var alt = $(this).attr('alt');
var content = $("#msgContent").val() + alt;
$("#msgContent").val(content);
$("#emojiBox").addClass("layui-hide");
});
});
$("#emojiBtn").click(function() {
$("#emojiBox").removeClass("layui-hide");
});
$("#emojiBox .layui-layer-close").click(function() {
$("#emojiBox").addClass("layui-hide");
});
// 图片上传
upload.render({
elem: '#uploadBtn',
url: '/chat/uploadImage',
accept: 'images',
exts: 'jpg|jpeg|png',
size: 1024 * 1024,
done: function(res) {
console.log(res);
if (res.code == 0) {
var message = new Object();
message.type = 2;
message.fromUserId = layim.cache().mine.id;
message.toUserId = getCurrentChatUserId();
message.content = '<img src="' + res.data + '" style="max-width: 300px;">';
message.sendTime = getNowTime();
socket.send(JSON.stringify(message));
appendChatMessage(message.toUserId, message);
} else {
layer.msg(res.msg, {icon: 5});
}
},
error: function() {
layer.msg('上传失败', {icon: 5});
}
});
// 发送消息
form.on('submit(sendMessage)', function(data) {
var message = new Object();
message.type = 2;
message.fromUserId = layim.cache().mine.id;
message.toUserId = getCurrentChatUserId();
message.content = data.field.content;
message.sendTime = getNowTime();
socket.send(JSON.stringify(message));
appendChatMessage(message.toUserId, message);
$("#msgContent").val('');
return false;
});
// 获取当前聊天用户 ID
function getCurrentChatUserId() {
var chatUserId = $("#chatList .layui-nav-item.layui-this").data('id');
if (chatUserId == undefined) {
chatUserId = 'all';
}
return chatUserId;
}
// 获取当前时间
function getNowTime() {
var date = new Date();
var year = date.getFullYear();
var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1;
var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;
}
// 追加聊天消息
function appendChatMessage(toUserId, message) {
var chatContent = $("#chatContent");
var chatMessage = '<div class="layui-row">';
if (message.fromUserId == layim.cache().mine.id) {
// 自己发送的消息
chatMessage += '<div class="layui-col-md12 layui-col-offset-8">' +
'<div class="layui-card">' +
'<div class="layui-card-header">' + message.sendTime + '</div>' +
'<div class="layui-card-body">' + message.content + '</div>' +
'</div>' +
'</div>';
} else {
// 收到的消息
chatMessage += '<div class="layui-col-md12">' +
'<div class="layui-card">' +
'<div class="layui-card-header">' + message.sendTime + '</div>' +
'<div class="layui-card-body">' + message.content + '</div>' +
'</div>' +
'</div>';
}
chatMessage += '</div>';
chatContent.append(chatMessage);
chatContent.scrollTop(chatContent[0].scrollHeight);
}
});
</script>
</body>
五、总结
本文介绍了基于 SpringBoot、WebSocket、Layui 和 Vue.js 的聊天交流模块的设计与实现,并提供了代码示例,希望能够对读者构建类似的聊天系统提供参考。
六、扩展
-
增加文件上传功能,支持发送图片、视频、音频等文件。
-
增加私聊消息加密功能,保护用户隐私。
-
增加群组管理功能,支持创建、加入、退出群组,以及群组成员管理等操作。
-
增加消息提醒功能,及时提醒用户有新消息。
-
增加离线消息功能,确保用户即使不在线也能收到消息。
-
增加用户关系管理功能,支持添加好友、拉黑用户等操作。
-
增加聊天表情库,丰富聊天内容。
-
增加聊天主题切换功能,提高用户体验。
-
增加聊天机器人功能,提供智能化的聊天服务。
-
增加数据分析功能,分析用户的聊天行为,提供数据报表。
原文地址: https://www.cveoy.top/t/topic/nz4U 著作权归作者所有。请勿转载和采集!