spring boot+vue实现H5聊天室客服功能
spring boot+vue实现H5聊天室客服功能
h5效果图
vue效果图
功能实现
spring boot
+webSocket
实现- 官方地址 https://docs.spring.io/spring-framework/docs/5.0.8.RELEASE/spring-framework-reference/web.html#websocket
maven 配置文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.2.0.RELEASE</version> </parent> <groupId>org.example</groupId> <artifactId>webChat</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.78</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
webSocket
配置
package com.example.webchat.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; /** * @author Mr.Fang * @title: WebSocketConfig * @Description: web socket 配置 * @date 2021/11/14 13:12 */ @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "myHandler/") // 访问路径 .addInterceptors(new WebSocketHandlerInterceptor()) // 配置拦截器 .setAllowedOrigins("*"); // 跨域 } @Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); container.setMaxTextMessageBufferSize(8192); // 例如消息缓冲区大小、空闲超时等 container.setMaxBinaryMessageBufferSize(8192); return container; } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } }
消息处理类
package com.example.webchat.config; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.example.webchat.pojo.DataVo; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.AbstractWebSocketHandler; import java.io.IOException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * @author Mr.Fang * @title: MyHandler * @Description: 消息处理类 * @date 2021/11/14 13:12 */ public class MyHandler extends AbstractWebSocketHandler { private static int onlineCount = 0; // 线程安全 private static Map<String, WebSocketSession> userMap = new ConcurrentHashMap<>(); // 用户 private static Map<String, WebSocketSession> adminMap = new ConcurrentHashMap<>(); // 客服 /** * @Description: 连接成功之后 * @param session * @return void * @Author Mr.Fang * @date 2021/11/14 13:15 */ @Override public void afterConnectionEstablished(WebSocketSession session) throws IOException { addOnlineCount(); // 当前用户加 1 System.out.println(session.getId()); Map<String, Object> map = session.getAttributes(); Object token = map.get("token"); Object admin = map.get("admin"); DataVo dataVo = new DataVo(); dataVo.setCode(9001).setMsg("连接成功"); if (Objects.nonNull(admin)) { adminMap.put(session.getId(), session); // 添加客服 } else { // 分配客服 userMap.put(session.getId(), session); // 添加当前用户 distribution(dataVo); } dataVo.setId(session.getId()); System.out.println("用户连接成功:" + admin); System.out.println("用户连接成功:" + token); System.out.println("在线用户:" + getOnlineCount()); this.sendMsg(session, JSONObject.toJSONString(dataVo)); } /** * @param vo * @return void * @Description: 分配客服 * @Author Mr.Fang * @date 2021/11/14 13:13 */ private void distribution(DataVo vo) { if (adminMap.size() != 0) { Random random = new Random(); int x = random.nextInt(adminMap.size()); Set<String> values = adminMap.keySet(); int j = 0; for (String str : values) { if (j == x) { vo.setRecId(str); System.out.println("分配ID:" + str); break; } j++; } } } /** * @param session * @param message * @return void * @Description: 收发消息 * @Author Mr.Fang * @date 2021/11/14 13:13 */ @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { System.out.print("用户ID:" + session.getId()); String payload = message.getPayload(); System.out.println("接收到的数据:" + payload); DataVo dataVo = JSON.toJavaObject(JSON.parseObject(payload), DataVo.class); // json 转对象 if (Objects.isNull(dataVo.getRecId()) || dataVo.getRecId().equals("")) { // 用户客服为空 分配客服 WebSocketSession socketSession = adminMap.get(session.getId()); if (Objects.isNull(socketSession)) { this.distribution(dataVo); } } if (dataVo.getCode() == 9002) { if (Objects.nonNull(dataVo.getRecId())) { // user -> admin WebSocketSession socketSession = adminMap.get(dataVo.getRecId()); dataVo.setSelfId(session.getId()).setRecId(""); this.sendMsg(socketSession, JSONObject.toJSONString(dataVo)); } else if (Objects.nonNull(dataVo.getSelfId())) { // admin ->user WebSocketSession socketSession = userMap.get(dataVo.getSelfId()); dataVo.setRecId(session.getId()).setSelfId(""); this.sendMsg(socketSession, JSONObject.toJSONString(dataVo)); } } } /** * @param session * @param msg * @return void * @Description: 发送消息 * @Author Mr.Fang * @date 2021/11/14 13:14 */ private void sendMsg(WebSocketSession session, String msg) throws IOException { session.sendMessage(new TextMessage(msg)); } /** * @Description: 断开连接之后 * @param session * @param status * @return void * @Author Mr.Fang * @date 2021/11/14 13:14 */ @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { subOnlineCount(); // 当前用户加 1 adminMap.remove(session.getId()); userMap.remove(session.getId()); System.out.println("用户断开连接token:" + session.getId()); System.out.println("用户断开连接admin:" + session.getId()); System.out.println("在线用户:" + getOnlineCount()); } public static synchronized int getOnlineCount() { return onlineCount; } /** * @Description: 在线用户 +1 * @return void * @Author Mr.Fang * @date 2021/11/14 13:16 */ public static synchronized void addOnlineCount() { MyHandler.onlineCount++; } /** * @Description: 在线用户 -1 * @return void * @Author Mr.Fang * @date 2021/11/14 13:16 */ public static synchronized void subOnlineCount() { MyHandler.onlineCount--; } }
配置拦截器
package com.example.webchat.config; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; import javax.servlet.http.HttpServletRequest; import java.util.Map; import java.util.Objects; /** * @author Mr.Fang * @title: WebSocketHandlerInterceptor * @Description: 拦截器 * @date 2021/11/14 13:12 */ public class WebSocketHandlerInterceptor extends HttpSessionHandshakeInterceptor { /** * @param request * @param response * @param wsHandler * @param attributes * @return boolean * @Description: 握手之前 * @Author Mr.Fang * @date 2021/11/14 13:18 */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; HttpServletRequest re = servletRequest.getServletRequest(); Object token = re.getParameter("token"); Object admin = re.getParameter("admin"); if (Objects.isNull(token)) { return false; } re.getSession().setAttribute("admin", admin); re.getSession().setAttribute("token", token); return super.beforeHandshake(request, response, wsHandler, attributes); } /** * @param request * @param response * @param wsHandler * @param ex * @return boolean * @Description: 握手之后 * @Author Mr.Fang * @date 2021/11/14 13:18 */ @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) { super.afterHandshake(request, response, wsHandler, ex); } }
h5服务端
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>服务端</title> <style type="text/css"> #client { margin: 0px auto; width: 500px; } input { width: 80%; height: 40px; border-radius: 5px; border-color: #CCCCCC; outline: #01FA01; } #button { width: 84px; height: 46px; background-color: #5af3a5; color: #fff; font-size: 20px; border-radius: 5px; border: none; box-shadow: 1px 1px 1px 1px #ccc; cursor: pointer; outline: #01FA01; } </style> </head> <body> <div id="client"> <h1 style="text-align: center;">服务端发送消息</h1> <div id="content" contenteditable=true style="width: 500px;height: 500px;margin: 0px auto;border: 1px solid #000000;padding: 10px;border-radius: 10px;overflow: auto;"> </div> <div style="padding: 5px;0px"> <input type="" value="" /> <button id="button" type="button">发送</button> </div> </div> <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script> <script type="text/javascript"> $(() => { var pushData = { code: 9002, msg: '', selfId: '', }; var time = null; var path = 'ws://127.0.0.1:8009/myHandler/'; if (typeof(WebSocket) === "undefined") { alert('不支持websocket') return; } let id = Math.random(); // 随机数 // 实例化socket var webSocket = new WebSocket(path + '?token=' + id+'&admin=1'); // 监听连接 webSocket.onopen = function(event) { console.log(event); interval(); }; // 监听消息 webSocket.onmessage = function(event) { let data = JSON.parse(event.data); pushData.selfId = data.selfId; if (data.code == 9002) { $('#content').append( `<p style="text-align: right;"><span style="color:chocolate;">${data.msg}</span>:客户端</p>` ) } else if (data.code == 9001) { $('#content').append(`<p style="color:#a09b9b;text-align:center;" >连接成功</p>`); } console.log(event) }; // 监听错误 webSocket.onerror = function(event) { console.log(event) $('#content').append(`<p style="color:#a09b9b;text-align:center;" >连接错误</p>`); clearInterval(); }; // 发送消息 $('#button').click(() => { let v = $('input').val(); if (v) { pushData.code = 9002; pushData.msg = v; webSocket.send(JSON.stringify(pushData)); $('#content').append( `<p>服务端:<span style="color: blueviolet;">${v}</span></p>` ) $('input').val(''); } }) function interval() { time = setInterval(() => { pushData.code = 9003; pushData.msg = '心跳'; webSocket.send(JSON.stringify(pushData)); }, 5000); } function clearInterval() { clearInterval(time); } }) </script> </body> </html>
客户端
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>客户端</title> <style type="text/css"> #client { margin: 0px auto; width: 500px; } input { width: 80%; height: 40px; border-radius: 5px; border-color: #CCCCCC; outline: #01FA01; } #button { width: 84px; height: 46px; background-color: #5af3a5; color: #fff; font-size: 20px; border-radius: 5px; border: none; box-shadow: 1px 1px 1px 1px #ccc; cursor: pointer; outline: #01FA01; } </style> </head> <body> <div id="client"> <h1 style="text-align: center;">客户端发送消息</h1> <div id="content" contenteditable=true style="width: 500px;height: 500px;margin: 0px auto;border: 1px solid #000000;padding: 10px;border-radius: 10px;overflow: auto;"> </div> <div style="padding: 5px;0px"> <input type="" value="" /> <button id="button" type="button">发送</button> </div> </div> <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script> <script type="text/javascript"> $(() => { var pushData = { code: 9002, msg: '', recId: '', }; var time = null; var path = 'ws://127.0.0.1:8009/myHandler/'; if (typeof(WebSocket) === "undefined") { alert('不支持websocket') return; } let id = Math.random(); // 随机数 // 实例化socket var webSocket = new WebSocket(path + '?token=' + id); // 监听连接 webSocket.onopen = function(event) { console.log(event); interval(); }; // 监听消息 webSocket.onmessage = function(event) { let data = JSON.parse(event.data); if (data.code == 9002) { $('#content').append( `<p style="text-align: right;"><span style="color:chocolate;">${data.msg}</span>:服务端</p>` ) } else if (data.code == 9001) { $('#content').append(`<p style="color:#a09b9b;text-align:center;" >连接成功</p>`); } console.log(event) }; // 监听错误 webSocket.onerror = function(event) { console.log(event) $('#content').append(`<p style="color:#a09b9b;text-align:center;" >连接错误</p>`); clearInterval(); }; // 发送消息 $('#button').click(() => { let v = $('input').val(); if (v) { pushData.code = 9002; pushData.msg = v; webSocket.send(JSON.stringify(pushData)); $('#content').append( `<p>客户端:<span style="color: blueviolet;">${v}</span></p>` ) $('input').val(''); } }) function interval() { time = setInterval(() => { pushData.code = 9003; pushData.msg = '心跳'; webSocket.send(JSON.stringify(pushData)); }, 5000); } function clearInterval() { clearInterval(time); } }) </script> </body> </html>
vue
连接webSocket
<template> <div class="chat"> <van-nav-bar fixed placeholder title="聊天内容" left-arrow /> <div id="content" ref="rightBody"> <div v-for="item in list" :key="item.id"> <div class="chat-model" v-if="item.isSelf"> <div> <van-image width="45px" height="45px" fit="fill" round src="https://img01.yzcdn.cn/vant/cat.jpeg" /> </div> <div class="chat-content chat-content-l"> {{item.content}} </div> </div> <div class="chat-model" style="justify-content: flex-end" v-else> <div class="chat-content chat-content-r"> {{item.content}} </div> <div> <van-image width="45px" height="45px" fit="fill" round src="https://img01.yzcdn.cn/vant/cat.jpeg" /> </div> </div> </div> </div> <div id="bottom"> <input type="text" v-model="text" /> <van-button @click="onSend">发送</van-button> </div> </div> </template> <script> export default { name: 'HelloWorld', data() { return { path: "ws://192.168.31.156:8009/myHandler/", // socket 地址 socket: "", text: '', data: { code: 9002, msg: '', recId: '', }, list: [], time: '', // 定时器 } }, created() { this.init() }, methods: { onSend() { if (this.socket.readyState != 1) { this.$toast('连接失败请重新进入'); return; } if (!this.text) { this.$toast('请输入内容') return; } var data = { avator: 'https://img01.yzcdn.cn/vant/cat.jpeg', content: this.text, isSelf: false } this.list.push(data); this.send() this.text = ''; this.$refs.rightBody.scrollTop = this.$refs.rightBody.scrollHeight; }, init: function() { // 0 CONNECTING 连接尚未建立 // 1 OPEN WebSocket的链接已经建立 // 2 CLOSING 连接正在关闭 // 3 CLOSED 连接已经关闭或不可用 if (typeof(WebSocket) === "undefined") { this.$toast('您的浏览器不支持socket') } else { let id = Math.random(); // 随机数 // 实例化socket this.socket = new WebSocket(this.path + '?token=' + id); // 监听socket连接 this.socket.onopen = this.open // 监听socket错误信息 this.socket.onerror = this.error // 监听socket消息 this.socket.onmessage = this.getMessage // this.onHeartbeat(); // 心跳防止断开连接 } }, open: function() { this.$toast('连接成功') }, error: function() { this.$toast('连接失败') }, getMessage: function(res) { let t = JSON.parse(res.data); var data = { avator: 'https://img01.yzcdn.cn/vant/cat.jpeg', content: t.msg, isSelf: true } if (t.code == 9002) { this.list.push(data); } this.data.recId = t.recId; this.$refs.rightBody.scrollTop = this.$refs.rightBody.scrollHeight; }, send: function() { if (this.socket) { this.data.code = 9002; this.data.msg = this.text; this.socket.send(JSON.stringify(this.data)) } }, close: function() { console.log("socket已经关闭") }, onHeartbeat() { var time = setInterval(() => { this.data.code = 9003; this.data.msg = '心跳'; this.socket.send(JSON.stringify(this.data)) }, 5000); this.time = time; } }, destroyed() { // 销毁监听 clearInterval(this.time); this.socket.onclose = this.close } } </script> <style> .chat { height: 100vh; background-color: #f1f1f3; } #content { overflow: auto; height: 100vh; padding-bottom: 100px; background-color: #f1f1f3; } #bottom { position: fixed; bottom: 0px; width: 100%; display: flex; justify-content: space-evenly; padding: 10px 0px; background-color: #F1F1F3; } #bottom input { background-color: white; width: 72%; height: 30px; padding: 3px 5px; vertical-align: sub; border-style: none; border-radius: 5px; } #bottom button { height: 32px; background-color: rgb(245, 158, 1); border-radius: 5px; color: #fff; } .chat-model { display: flex; flex-direction: row; margin: 10px 10px; margin-top: 30px; align-items: center; } .chat-content { position: relative; max-width: 67%; word-break: break-all; word-wrap: break-word; top: 18px; padding: 10px; border-radius: 5px; background-color: white; } .chat-content-r { right: 10px; } .chat-content-l { left: 10px; } </style>
源码地址 https://gitee.com/bxmms/web-chat.git
哇!又赚了一天人民币
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· .NET 适配 HarmonyOS 进展
· .NET 进程 stackoverflow异常后,还可以接收 TCP 连接请求吗?
· 本地部署 DeepSeek:小白也能轻松搞定!
· 如何给本地部署的DeepSeek投喂数据,让他更懂你
· 在缓慢中沉淀,在挑战中重生!2024个人总结!
· 大人,时代变了! 赶快把自有业务的本地AI“模型”训练起来!
· 从 Windows Forms 到微服务的经验教训