聊天交流模块详细设计与实现采用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});
}
},
erro
原文地址: http://www.cveoy.top/t/topic/cPbl 著作权归作者所有。请勿转载和采集!