一、概述

本文将介绍一种基于 SpringBoot、WebSocket、layui、vue 的聊天交流模块的详细设计与实现。该模块的主要功能是实现在线聊天,支持群聊和私聊,同时提供聊天记录的保存和查询功能。

二、系统架构

  1. 前端页面

前端页面采用 layui 和 vue 框架,通过 WebSocket 与后端进行通信。

  1. 后端服务

后端服务采用 SpringBoot 框架,并通过 WebSocket 处理客户端的聊天请求。同时,通过 Mybatis 对数据库进行操作,实现聊天记录的保存和查询功能。

三、模块设计

  1. 数据库设计

本模块使用 MySQL 数据库,设计了以下两张表:

1)user 表:保存用户信息,包括用户 ID、用户名、密码等字段。

2)chat_record 表:保存聊天记录,包括发送者 ID、接收者 ID、消息内容、发送时间等字段。

  1. 后端服务设计

1)WebSocket 处理

后端服务使用 WebSocket 处理客户端的聊天请求。通过 @ServerEndpoint 注解标注一个 WebSocket 处理器类,接收客户端的连接请求。

2)消息处理

后端服务通过 @OnMessage 注解标注一个方法,接收客户端发送的消息,并进行处理。根据消息类型,将消息转发给指定的用户或群组。

3)持久化

后端服务使用 Mybatis 进行数据库操作,将聊天记录保存到 chat_record 表中,并提供查询接口,供客户端查询历史聊天记录。

  1. 前端页面设计

1)登录页面

用户在登录页面输入用户名和密码,验证通过后进入聊天界面。

2)聊天界面

聊天界面分为左右两个区域,左侧为用户列表,右侧为聊天窗口。用户列表中显示当前在线的用户和群组,用户可以选择私聊或群聊。聊天窗口中显示当前聊天的内容,用户可以发送消息、表情和图片。

四、模块实现

  1. 数据库实现

使用 MySQL 数据库,创建 user 表和 chat_record 表,并插入测试数据。

  1. 后端服务实现

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. 前端页面实现

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">&#xe67c;</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 的聊天交流模块的设计与实现,并提供了代码示例,希望能够对读者构建类似的聊天系统提供参考。

六、扩展

  1. 增加文件上传功能,支持发送图片、视频、音频等文件。

  2. 增加私聊消息加密功能,保护用户隐私。

  3. 增加群组管理功能,支持创建、加入、退出群组,以及群组成员管理等操作。

  4. 增加消息提醒功能,及时提醒用户有新消息。

  5. 增加离线消息功能,确保用户即使不在线也能收到消息。

  6. 增加用户关系管理功能,支持添加好友、拉黑用户等操作。

  7. 增加聊天表情库,丰富聊天内容。

  8. 增加聊天主题切换功能,提高用户体验。

  9. 增加聊天机器人功能,提供智能化的聊天服务。

  10. 增加数据分析功能,分析用户的聊天行为,提供数据报表。

SpringBoot + WebSocket + Layui + Vue 构建聊天交流模块详细设计与实现

原文地址: https://www.cveoy.top/t/topic/nz4U 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录