基于websocket实现一个简单的网站在线客服聊天室案例(代码片段)

基于websocket实现一个简单的网站在线客服聊天室案例(代码片段)

前言

 

在网站项目开发中,我们经常会有在线客服的功能,通过网站开启一个聊天室,进行客服解答功能,本节我们使用websocket实现一个简单的网站在线客服聊天功能,效果如下:

正文

  • 后端引入websocket的pom依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

  •  后端初始化注入websocket的ServerEndpointExporter端点
package com.yundi.atp.platform.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
 * @Author: yanp
 * @Description:
 * @Date: 2021/9/15 15:27
 * @Version: 1.0.0
 */
@Configuration
public class WebSocketConfig 
    /**
     * 注入ServerEndpointExporter,
     * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() 
        return new ServerEndpointExporter();
    

  •  后端创建一个websocket服务器,用于前后端通信,发送消息
package com.yundi.atp.platform.websocket;

import com.alibaba.fastjson.JSON;
import com.yundi.atp.platform.module.test.entity.ChatMsg;
import com.yundi.atp.platform.module.test.service.ChatMsgService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @Author: yanp
 * @Description: WebSocket服务端
 * @Date: 2021/9/15 15:27
 * @Version: 1.0.0
 */
@Slf4j
@Component
@ServerEndpoint("/websocket/chat/userName")
public class WebSocketServer 
    private Session session;

    private static CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();

    private static Map<String, Session> sessionPool = new ConcurrentHashMap<>();

    private static ChatMsgService chatMsgService;

    private static String SUPER_ADMIN = "super_admin";

    @Autowired
    public void setWebSocketServer(ChatMsgService chatMsgService) 
        WebSocketServer.chatMsgService = chatMsgService;
    

    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userName") String userName) 
        List<String> onlineList = new ArrayList<>();
        this.session = session;
        webSockets.add(this);
        sessionPool.put(userName, session);
        log.info("【websocket消息】有新的连接,总数为:" + webSockets.size());
        Iterator<Map.Entry<String, Session>> iterator = sessionPool.entrySet().iterator();
        while (iterator.hasNext()) 
            onlineList.add(iterator.next().getKey());
        
        onlineList.remove(SUPER_ADMIN);
        Map<String, Object> map = new HashMap<>(16);
        map.put("key", 1);
        map.put("onlineList", onlineList);
        map.put("userList", chatMsgService.getUserList());
        this.sendOneMessage(SUPER_ADMIN, JSON.toJSONString(map));
    

    @OnClose
    public void onClose(@PathParam(value = "userName") String userName) 
        webSockets.remove(this);
        sessionPool.remove(userName);
        log.info("【websocket消息】连接断开,总数为:" + webSockets.size());
        List<String> onlineList = new ArrayList<>();
        Iterator<Map.Entry<String, Session>> iterator = sessionPool.entrySet().iterator();
        while (iterator.hasNext()) 
            onlineList.add(iterator.next().getKey());
        
        onlineList.remove(SUPER_ADMIN);
        Map<String, Object> map = new HashMap<>(16);
        map.put("key", 2);
        map.put("onlineList", onlineList);
        map.put("userList", chatMsgService.getUserList());
        this.sendOneMessage(SUPER_ADMIN, JSON.toJSONString(map));
    

    @OnMessage
    public void onMessage(String message) 
        ChatMsg chatMsg = JSON.parseObject(message, ChatMsg.class);
        chatMsgService.save(chatMsg);
        Map<String, Object> map = new HashMap<>(16);
        map.put("key", 3);
        map.put("data", chatMsg);
        this.sendOneMessage(chatMsg.getSender(), JSON.toJSONString(map));
        this.sendOneMessage(chatMsg.getReceiver(), JSON.toJSONString(map));
    

    /**
     * 广播消息
     */
    public void sendAllMessage(String message) 
        for (WebSocketServer webSocket : webSockets) 
            log.info("【websocket消息】广播消息:" + message);
            try 
                webSocket.session.getAsyncRemote().sendText(message);
             catch (Exception e) 
                e.printStackTrace();
            
        
    

    /**
     * 单点消息
     *
     * @param userName
     * @param message
     */
    public void sendOneMessage(String userName, String message) 
        log.info("【websocket消息】单点消息:" + message);
        Session session = sessionPool.get(userName);
        if (session != null) 
            try 
                session.getAsyncRemote().sendText(message);
             catch (Exception e) 
                e.printStackTrace();
            
        
    


  • 后端聊天室功能控制层接口
package com.yundi.atp.platform.module.test.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yundi.atp.platform.common.Result;
import com.yundi.atp.platform.module.test.entity.ChatMsg;
import com.yundi.atp.platform.module.test.service.ChatMsgService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author yanp
 * @since 2021-09-17
 */
@Api(tags = "聊天室接口")
@RestController
@RequestMapping("/test/chatMsg")
public class ChatMsgController 
    @Autowired
    private ChatMsgService chatMsgService;

    @ApiOperation(value = "获取聊天室地址")
    @GetMapping(value = "/getWebSocketAddress/username")
    public Result getWebSocketAddress(HttpServletRequest request, @PathVariable(value = "username") String username) throws UnknownHostException 
        String address = "ws://" + InetAddress.getLocalHost().getHostAddress() + ":" + request.getServerPort() + request.getContextPath() + "/websocket/chat/" + username;
        return Result.success(address);
    

    @ApiOperation(value = "获取历史聊天记录")
    @GetMapping(value = "/getHistoryChat/username")
    public Result getWebSocketAddress(@PathVariable(value = "username") String username) 
        List<ChatMsg> list = chatMsgService.list(new QueryWrapper<ChatMsg>()
                .and(wrapper -> wrapper.eq("sender", username).or().eq("receiver", username))
                .orderByDesc("create_time"));
        List<ChatMsg> collect = list.stream().sorted(Comparator.comparing(ChatMsg::getCreateTime)).collect(Collectors.toList());
        return Result.success(collect);
    

    @ApiOperation(value = "获取用户列表")
    @GetMapping(value = "/getUserList")
    public Result getUserList() 
        List<String> userList = chatMsgService.getUserList();
        return Result.success(userList);
    



  •  后端聊天室功能业务层
package com.yundi.atp.platform.module.test.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yundi.atp.platform.module.test.entity.ChatMsg;
import com.yundi.atp.platform.module.test.mapper.ChatMsgMapper;
import com.yundi.atp.platform.module.test.service.ChatMsgService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author yanp
 * @since 2021-09-17
 */
@Service
public class ChatMsgServiceImpl extends ServiceImpl<ChatMsgMapper, ChatMsg> implements ChatMsgService 
    @Autowired
    private ChatMsgMapper chatMsgMapper;

    @Override
    public List<String> getUserList() 
        List<String> userList = chatMsgMapper.getUserList();
        return userList;
    

  •   后端聊天室功能持久层
package com.yundi.atp.platform.module.test.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yundi.atp.platform.module.test.entity.ChatMsg;

import java.util.List;

/**
 * <p>
 * Mapper 接口
 * </p>
 *
 * @author yanp
 * @since 2021-09-17
 */
public interface ChatMsgMapper extends BaseMapper<ChatMsg> 
    /**
     * 用户列表
     * @return
     */
    List<String> getUserList();


  •  后端聊天室功能持久层Mapper
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yundi.atp.platform.module.test.mapper.ChatMsgMapper">

    <select id="getUserList" resultType="java.lang.String">
        SELECT sender FROM `chat_msg` where sender != 'super_admin' GROUP BY sender
    </select>
</mapper>

  •   前端聊天室功能页面
<template>
  <div class="container">
    <el-card class="box-card">
      <div slot="header">
        <el-row type="flex">
          <el-col :span="1" style="margin: 15px 10px;">
            <img alt="ATP客服" src="@/assets/logo.png" style="width:40px;height:40px;"/>
          </el-col>
          <el-col :span="3" style="line-height: 74px;margin-left: 10px;">
            <span style="display: inline-block;color: white;">ATP客服</span>
          </el-col>
          <el-col :span="20" v-if="username==='super_admin'">
            <h5 style="color: #83ccd2;padding: 0;text-align: right;margin: 50px 20px 0 0;">当前在线人数: online </h5>
          </el-col>
          <el-col :span="20" v-else>
            <h5 style="color: #83ccd2;padding: 0 0 2px 0;text-align: right;margin: 50px 20px 0 0;font-size: 18px;">
               username </h5>
          </el-col>
        </el-row>
      </div>
      <div class="content" ref="content">
        <el-row type="flex">
          <el-col :span="6" style="background: #eee;min-height: 600px;" v-if="username==='super_admin'">
            <el-tabs v-model="activeName" @tab-click="handleClick" style="position: fixed;width: 190px;margin: 0 2px;">
              <el-tab-pane label="在线用户" name="online">
                <div v-for="item in friend" :key="item" @click="switchUser(item)" :class="item===active?'mark':''">
                  <el-badge :is-dot=msgNotify.includes(item) class="item" type="success">
                    <li style="list-style-type:none;padding: 5px 8px;cursor: pointer;"
                        class="active">
                       item 
                    </li>
                  </el-badge>
                  <el-divider></el-divider>
                </div>
              </el-tab-pane>
              <el-tab-pane label="全部用户" name="all">
                <div v-for="item in userList" :key="item" @click="switchUser(item)" :class="item===active?'mark':''">
                  <el-badge :is-dot=msgNotify.includes(item) class="item" type="success">
                    <li style="list-style-type:none;padding: 5px 8px;cursor: pointer;"
                        :class="friend.includes(item)?'active':''">
                       item 
                    </li>
                  </el-badge>
                  <el-divider></el-divider>
                </div>
              </el-tab-pane>
            </el-tabs>
          </el-col>
          <el-col :span="18" v-if="username==='super_admin'">
            <div v-for="item in chatMsgList">
              <el-row type="flex" style="margin-bottom: 20px;" v-if="username===item.sender">
                <el-col :span="2">
                  <img alt="ATP客服" src="@/assets/logo.png" style="width:30px;height:30px;margin: 10px 0px 0px 20px;"/>
                </el-col>
                <el-col :span="22">
                  <el-row type="flex" style="margin-top: 10px;margin-left: 5px;opacity: 0.2;">
                    <el-col :span="7"><span style="padding-left: 20px;"> item.sender </span></el-col>
                    <el-col :span="7"><span> item.createTime | dataFormat('yyyy-MM-dd HH:mm') </span></el-col>
                  </el-row>
                  <el-row>
                    <el-col :span="14" style="margin-left: 8px;margin-top: 5px;">
                      <el-card style="padding: 8px 5px;">
                         item.msg 
                      </el-card>
                    </el-col>
                  </el-row>
                </el-col>
              </el-row>
              <el-row type="flex" style="margin-bottom: 20px;" v-else justify="end">
                <el-col :span="22">
                  <el-row type="flex" style="margin-top: 10px;margin-right: 5px;opacity: 0.2;" justify="end">
                    <el-col :span="6"><span> item.sender </span></el-col>
                    <el-col :span="7"><span> item.createTime | dataFormat('yyyy-MM-dd HH:mm') </span></el-col>
                  </el-row>
                  <el-row type="flex" justify="end" style="margin-right: 8px;margin-top: 5px;">
                    <el-col :span="14" style="margin-right: 8px;">
                      <el-card style="padding: 8px 5px;">
                         item.msg 
                      </el-card>
                    </el-col>
                  </el-row>
                </el-col>
                <el-col :span="2">
                  <el-avatar shape="square" size="medium" style="float: right;margin: 10px 20px 0px 0px;">客户</el-avatar>
                </el-col>
              </el-row>
            </div>
          </el-col>
          <el-col :span="24" v-else>
            <div v-for="item in chatMsgList">
              <el-row type="flex" style="margin-bottom: 20px;" v-if="username===item.sender">
                <el-col :span="2">
                  <el-avatar shape="square" size="medium" style="float: right;margin: 10px 20px 0px 0px;">客户</el-avatar>
                </el-col>
                <el-col :span="22">
                  <el-row type="flex" style="margin-top: 10px;opacity: 0.2;margin-left: 20px;">
                    <el-col :span="7"><span style="padding-left: 5px;"> item.sender </span></el-col>
                    <el-col :span="7"><span> item.createTime | dataFormat('yyyy-MM-dd HH:mm') </span></el-col>
                  </el-row>
                  <el-row>
                    <el-col :span="14">
                      <el-card style="padding: 8px 5px;">
                         item.msg 
                      </el-card>
                    </el-col>
                  </el-row>
                </el-col>
              </el-row>
              <el-row type="flex" style="margin-bottom: 20px;" v-else justify="end">
                <el-col :span="22">
                  <el-row type="flex" style="margin-top: 10px;margin-right: 5px;opacity: 0.2;" justify="end">
                    <el-col :span="6"><span> item.sender </span></el-col>
                    <el-col :span="7"><span> item.createTime | dataFormat('yyyy-MM-dd HH:mm') </span></el-col>
                  </el-row>
                  <el-row type="flex" justify="end" style="margin-top: 5px;">
                    <el-col :span="14">
                      <el-card style="padding: 8px 5px;">
                         item.msg 
                      </el-card>
                    </el-col>
                  </el-row>
                </el-col>
                <el-col :span="2">
                  <img alt="ATP客服" src="@/assets/logo.png" style="width:30px;height:30px;margin: 10px 0px 0px 20px;"/>
                </el-col>
              </el-row>
            </div>
          </el-col>
        </el-row>
      </div>
      <div class="operate" v-if="username==='super_admin'">
        <el-input
            type="textarea"
            :autosize=" minRows: 3, maxRows: 3"
            placeholder="您好!这里是ATP客服部,我是客服小美,很高兴为您服务!"
            v-model="msg">
        </el-input>
        <el-button type="success" size="mini" style="float: right;margin-top: 5px;" @click="sendMsg"
                   :disabled="!(msg && active)">
          发送
        </el-button>
      </div>
      <div class="operate" v-else>
        <el-input
            type="textarea"
            :autosize=" minRows: 3, maxRows: 3"
            placeholder="您好!这里是ATP客服部,我是客服小美,很高兴为您服务!"
            v-model="msg">
        </el-input>
        <el-button type="success" size="mini" style="float: right;margin-top: 5px;" @click="sendMsg" :disabled="!msg">
          发送
        </el-button>
      </div>
    </el-card>
  </div>
</template>

<script>
export default 
  name: "ClientChat",
  data() 
    return 
      msg: '',
      chatMsgList: [],
      username: sessionStorage.getItem("username"),
      friend: [],
      online: 0,
      active: '',
      receiver: 'super_admin',
      userList: [],
      activeName: 'online',
      msgNotify:[],
    
  ,
  created() 
    this.getWebSocketAddress();
  ,
  methods: 
    //tab切换
    handleClick(tab, event) 
      const _this = this;
      if (tab.name === 'online') 
        if (!_this.active) 
          if (_this.online > 0) 
            _this.active = _this.friend[0];
            _this.activeName = 'online';
            _this.receiver = _this.active;
            _this.getHistoryChat(_this.receiver);
           else 
            if (_this.userList.length > 0) 
              _this.active = _this.userList[0];
              _this.activeName = 'all';
              _this.receiver = _this.active;
              _this.getHistoryChat(_this.receiver);
            
          
        
      
      if (tab.name === 'all') 
        if (!_this.active) 
          if (_this.online > 0) 
            _this.active = _this.friend[0];
            _this.activeName = 'online';
            _this.receiver = _this.active;
            _this.getHistoryChat(_this.receiver);
           else 
            if (_this.userList.length > 0) 
              _this.active = _this.userList[0];
              _this.activeName = 'all';
              _this.receiver = _this.active;
              _this.getHistoryChat(_this.receiver);
            
          
        
      
    ,
    //切换用户
    switchUser(data) 
      if (this.active === data) 
        return;
      
      this.active = data;
      this.receiver = data;
      //获取历史聊天记录
      this.getHistoryChat(this.receiver);
      this.msgNotify = this.msgNotify.filter(item => item != this.active);
    ,
    //获取历史聊天记录
    getHistoryChat(data) 
      this.$http.get('/test/chatMsg/getHistoryChat/' + data).then(res => 
        if (res.data.code === 1) 
          this.chatMsgList = res.data.data;
          this.flushScroll();
         else 
          this.$message.warning(res.data.msg);
        
      ).catch(error => 
        this.$message.error(error);
      );
    ,
    //获取websocket地址
    getWebSocketAddress() 
      this.$http.get('/test/chatMsg/getWebSocketAddress/' + this.username).then(res => 
        if (res.data.code === 1) 
          if ('WebSocket' in window) 
            this.websocket = new WebSocket(res.data.data);
            this.initWebSocket();
            if (this.username != 'super_admin') 
              this.getHistoryChat(this.username);
            
           else 
            this.$message.warning('当前浏览器不支持websocket创建!');
          
         else 
          this.$message.warning(res.data.msg);
        
      ).catch(error => 
        this.$message.error(error);
      );
    ,
    //初始化websocket
    initWebSocket() 
      const _this = this;
      _this.websocket.onerror = function (event) 
        _this.$message.error('服务端连接错误!');
      
      _this.websocket.onopen = function (event) 
        _this.$message.success("连接成功!");
      
      _this.websocket.onmessage = function (event) 
        let res = JSON.parse(event.data);
        if (res.key === 1) 
          _this.userList = res.userList;
          _this.friend = res.onlineList;
          _this.online = _this.friend.length;
          if (!_this.active) 
            if (_this.online > 0) 
              _this.active = _this.friend[0];
              _this.activeName = 'online';
              _this.receiver = _this.active;
              _this.getHistoryChat(_this.receiver);
             else 
              if (_this.userList.length > 0) 
                _this.active = _this.userList[0];
                _this.activeName = 'all';
                _this.receiver = _this.active;
                _this.getHistoryChat(_this.receiver);
              
            
          
        
        if (res.key === 2) 
          _this.userList = res.userList;
          _this.friend = res.onlineList;
          _this.online = _this.friend.length;
          if (!_this.active) 
            if (_this.online > 0) 
              _this.active = _this.friend[0];
              _this.activeName = 'online';
              _this.receiver = _this.active;
              _this.getHistoryChat(_this.receiver);
             else 
              if (_this.userList.length > 0) 
                _this.active = _this.userList[0];
                _this.activeName = 'all';
                _this.receiver = _this.active;
                _this.getHistoryChat(_this.receiver);
              
            
          
        
        if (res.key === 3) 
          if (_this.username === res.data.sender) 
            _this.chatMsgList.push(res.data);
            _this.flushScroll();
           else 
            if (res.data.sender === 'super_admin') 
              _this.chatMsgList.push(res.data);
              _this.flushScroll();
             else 
              if (res.data.sender === _this.active) 
                _this.chatMsgList.push(res.data);
                _this.flushScroll();
               else 
                //发送其它用户处理
                _this.msgNotify.push(res.data.sender);
              
            
          
        
      
      _this.websocket.onclose = function (event) 
        _this.$message.warning('服务端已关闭!');
      
    ,
    //发送消息
    sendMsg() 
      if (this.msg.trim().length === 0) 
        this.$message.warning('不能发送空消息!');
        return;
      
      let chatMsg = ;
      chatMsg.msg = this.msg;
      chatMsg.sender = this.username;
      chatMsg.createTime = new Date();
      chatMsg.receiver = this.receiver;
      this.websocket.send(JSON.stringify(chatMsg));
      this.msg = '';
      this.flushScroll();
    ,
    //刷新滚动条
    flushScroll() 
      let content = this.$refs.content;
      setTimeout(() => 
        content.scrollTop = content.scrollHeight;
      , 100);
    ,
  

</script>

<style scoped lang="scss">
.container 
  padding-top: 50px;

  .box-card 
    margin: auto;
    width: 800px;
    height: 800px;
    max-height: 900px;

    ::v-deep .el-card__header 
      background: #867ba9 !important;
      border-bottom: none;
      padding: 0;
    

    ::v-deep .el-card__body 
      padding: 0px !important;
      position: relative;

      .content 
        height: 600px;
        background: #ddd;
        overflow-y: auto;

        .el-divider--horizontal 
          margin: 0;
        

        .active 
          color: #0080ff;
        

        .mark 
          background: #deb068;
        

        .item 
          margin-top: 10px;
          margin-right: 10px;
        
      

      .operate 
        padding: 5px 15px;
      
    
  

</style>

  •  聊天室页面效果

结语

本节内容到这里就结束了,我们下期见。。。

相关内容

赞(1)

文章来源于网络,原文链接请点击 这里
文章版权归作者所有,如作者不同意请直接联系小编删除。
作者:北溟溟