spring boot+vue实现H5聊天室客服功能

spring boot+vue实现H5聊天室客服功能

h5效果图

vue效果图

功能实现

maven 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="//maven.apache.org/POM/4.0.0"
         xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //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="//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="//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="//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="//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: '//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: '//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>

源码地址 //gitee.com/bxmms/web-chat.git