一、概述

本文将介绍一种基于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});
                    }
                },
                erro
聊天交流模块详细设计与实现采用springboot、WebSocket、layui、vue

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

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