利用websocket实现web端在线客服实时聊天系统

专栏简介
💒个人主页

📖心灵鸡汤📖
沙漠之所以美丽,是因为在不远处有一片绿洲。
✍相关博文✍
利用webSocket实现扫码登录PC端

需求场景模拟

1.移动端给客服发送消息,客户在bs端后台收到消息并回复(本文以一个客服为例)
2.左侧聊天栏显示最新的消息和消息时间
3.需要查看对方是否已读自己的消息

开发需求

一、技术选型

使用websocket进行消息推送
优点:个人感觉开发简单,不需要部署第三方服务。
缺点:无状态,页面每刷新一次就会产生一个新的session,某些情况下不稳定。

还是那句话,技术没有什么好与坏,只有合适不合适,同时最终采用技术栈意味着你能忍受它的缺点

二、需求分析

1.发送消息意味着需要发送人和接收人两个角色,同时需要对用户进行持久化。
2.对接收人(发送人)来说,显示最新的消息和时间,就意味着显示双方消息记录的最后一条消息的内容和发送时间
3.消息已读意味着打开聊天对话框就要告诉对方,自己已读对方的消息。这里会产生两种情况:
己方在线对方未在线,需要在对方上线时(即打开对话框)告诉对方自己已读对方的消息。
解决方案:存储消息数据,在自己打开对框的时候,获取聊天记录,并将聊天记录中对方给自己发的消息状态全部更新为已读。
双方同时在线(聊天对话界面),这里稍微有点操作,那就是如何让双方及时的知道对方已读自己的消息。
场景及解决方案:
场景:当自己给对方发送消息时,自己看到自己发给对方的消息已读(即便对方不给自己发消息)
解决:
1.自己向对方发消息时,更新对方发给自己的全部消息为已读,对方读取消息。
2.对方读取消息时,让对方告诉自己对方已读自己的消息,自己进行已读展示,通过一个共用接口即可,当对方调用指定接口时,让对方告诉自己对方已读即可。
4.利用mongodb进行用户以及聊天记录的存储。

效果演示

消息聊天演示:
请添加图片描述
消息时间演示:
在这里插入图片描述
消息未读演示:
在这里插入图片描述

软件需求实现

1.技术架构

🖥️PC端:vue+springboot
📱移动端:html+springboot

2.实现流程图:(仅供参考)

在这里插入图片描述

一、数据库设计

1.sys_user

字段说明
userId用户id
nickName昵称
heardUrl头像地址
sex性别

2.chat_msg

字段说明
createTime创建时间
msgKey通信key(sendId_receiveId)
chatMsg通信消息
sendId发送id
receiveId接收id
readState查看状态 0未看 1已看

二、代码实现

说明:代码有所删减修改,但核心代码存在!!!请选择性复用代码

1.web端

引入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-websocket</artifactId>
            <version>9.0.39</version>
        </dependency>

配置yml

#开发时服务端口号,部署无效
server:
  port: 9019
  servlet: 
    context-path: fmis
spring:
  data:
    mongodb:
      host: 192.168.1.125
      port: 27017
      database: cangzhou
1.前端代码

index.vue

<template>
  <div :class="classObj" class="app-wrapper">
    <Logo></Logo>
    <div class="clearfix"></div>
    <sidebar class="sidebar-container" />
    <div class="communicate" >
      <el-button type="text" @click="talkRoomVisible = true" :class="{ breathing: unReadMessageNum && talkRoomVisible ==false}">
        //对话图标
        <img src="../../static/images/login/tips.png" alt />
      </el-button>
    </div>
    <CommunicationInterface
      style="
        position: absolute;
        z-index: 1000;
        top: 0;
        right: 0;
        left: 0;
        margin: auto;
      "
      v-if="talkRoomVisible"
      :userList="userList"
    />
  </div>
</template>

<script>
import message from "./mixin/message";
import CommunicationInterface from "@/dialog/chat.vue";
export default {
  name: "Layout",
  components: {
    Navbar,
    Sidebar,
    AppMain,
    Logo,
    CommunicationInterface,
  },
  data() {
    return {};
  },
  mixins: [ResizeMixin, message],
  computed: {
    sidebar() {
      return this.$store.state.app.sidebar;
    },
    device() {
      return this.$store.state.app.device;
    },
    fixedHeader() {
      return this.$store.state.settings.fixedHeader;
    },
    classObj() {
      return {
        hideSidebar: !this.sidebar.opened,
        openSidebar: this.sidebar.opened,
        withoutAnimation: this.sidebar.withoutAnimation,
        mobile: this.device === "mobile",
      };
    },
    unreadList() {
      return this.userList.filter((e) => {
        return e.unReadMsgNum > 0;
      });
    },
  },
  methods: {
    handleClickOutside() {
      this.$store.dispatch("app/closeSideBar", { withoutAnimation: false });
    },
  },
};
</script>

<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
@import "~@/styles/variables.scss";
.breathing {
  animation: breathe 1s infinite;
}
@keyframes breathe {
  0% {
    opacity: 0.1;
  }
  50% {
    opacity: 1;
  }
  100% {
    opacity: 0.1;
  }
}
.hoverVisibleUnreadList{
  position: absolute;
  bottom: 35px;
  right: -50px;
  width: 180px;
  line-height: 35px;
  display: none;
  border-radius: 3px;
 box-shadow: 0 0 2px #888888;
}
.communicate:hover .hoverVisibleUnreadList{
  display: block
}
.app-wrapper {
  @include clearfix;
  position: relative;
  height: 100%;
  width: 100%;
  &.mobile.openSidebar {
    position: fixed;
    top: 0;
  }
}
.drawer-bg {
  background: #000;
  opacity: 0.3;
  width: 100%;
  top: 0;
  height: 100%;
  position: absolute;
  z-index: 999;
}

.fixed-header {
  position: fixed;
  top: 0;
  right: 0;
  z-index: 9;
  width: calc(100% - 0); // calc(100% - #{$sideBarWidth})
  transition: width 0.28s;
}

.hideSidebar .fixed-header {
  width: calc(100% - 0px);
}

.mobile .fixed-header {
  width: 100%;
}
.communicate {
  position: absolute;
  z-index: 2000;
  height: 50px;
  width: 50px;
  bottom: 50px;
  right: 50px;
  border-radius: 50%;
  img {
    width: 50px;
  }
}
</style>

chat.vue
<template>
  <div>
    <div class="box">
      <div class="min">
        <ul class="contactList">
          <h3 style="border-bottom: 0.5px solid #337bf1">联系人列表</h3>
          <li
            v-for="item in userListSortByLastTime"
            :key="item.nickName"
            @click="CreateConnection(item)"
            :class="{ red: item.userId === userId }"
          >
            <div class="leftLi">
              <!-- <img
                :src="require('../../../static/images/login/titleboy.png')"
              /> -->
              <img :src="item.heardUrl" />
              <div class="ListItem">
                <div>
                  <div class="nickName">{{ item.nickName }}</div>
                  <span style="vertical-align: top">{{
                    item.lastMsgTime | dateFormat
                  }}</span>
                </div>
                <div class="ellipsis">
                  {{ item.lastMsg }}
                  <span v-if="item.unReadMsgNum > 0">{{
                    [item.unReadMsgNum]
                  }}</span>
                </div>
              </div>
              <div class="delete" @click="delUserByUserId(item)">删除</div>
            </div>
          </li>
        </ul>
        <div class="chat">
          <h3>{{ contactsName || "聊天区域" }}</h3>
          <div class="content" ref="reference">
            <ul style="color: black">
              <li
                v-for="item in content"
                :key="item.id"
                :style="item.flag ? 'text-align: right;' : 'text-align: left;'"
              >
                <span>
                  <!-- <img
                    v-show="!item.flag"
                    :src="require('../../../static/images/login/titleboy.png')"
                  /> -->
                  <img
                    v-show="!item.flag"
                    :src="heardUrl"
                  />
                  <span class="times" v-show="item.flag">{{
                    item.createTime
                      ? item.createTime.substr(11, item.createTime.length)
                      : ""
                  }}</span>
                  <span :class="item.flag ? 'i' : 'he'">{{ item.name }}</span>
                  <span class="time" v-show="!item.flag">{{
                    item.createTime
                      ? item.createTime.substr(11, item.createTime.length)
                      : ""
                  }}</span>
                  <img
                    v-show="item.flag"
                    :src="require('../../../static/images/login/titleboy.png')"
                  />
                  <!-- <img v-show="item.flag" :src="heardUrl" /> -->
                  <div class="readOrUnread" v-if="item.flag">
                    {{ item.readState == "1" ? "已读" : "未读" }}
                  </div>
                </span>
              </li>
            </ul>
          </div>
          <div class="input">
            <textarea
              cols="50"
              rows="10"
              v-model="text"
              @keydown.enter="submit"
            ></textarea>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
// import axios from 'axios'
import * as API from "@/api/systemApplication";
import { dateFormat } from "@/utils/index";

export default {
  props: {
    dialogVisible: {
      type: Boolean,
      default: false,
    },
    userList: {
      type: Array,
      required: true,
    },
  },
  data() {
    return {
      // dialogVisible: true
      c: null,
      text: null, //输入的消息
      content: [], //内容列表
      scoket: "",
      sendId: 1,
      userId: "",
      contactsName: "",
      URL: window.ROOT,
      heardUrl: "", //右侧聊天框头像
    };
  },
  computed: {
    userListSortByLastTime() {
      // 做排序,最新接收到的消息放在列表的最上面,需要后端提供年月日时分秒
      return this.userList.sort((a, b) => {
        let at = a.lastMsgTime ? `${a.lastMsgTime}` : "1970-01-01 00:00:00";
        // console.log(at,"at")
        let bt = b.lastMsgTime ? `${b.lastMsgTime}` : "1970-01-01 00:00:00";
        // console.log(bt,"bt")
        return new Date(bt).getTime() - new Date(at).getTime();
      });
    },
  },
  mounted() {
    // 因为上面用了v-if 所以每次这个组件出来 都会走mounted
    this.userId = this.userList[0].userId;
    this.heardUrl = this.userList[0].heardUrl;
    // 获取当前聊天人的消息
    this.getSelectedUserMessageList();
    // 接收ws消息
    this.$root.$on("pushNewMessage", this.pushNewMessage);

    this.$root.$emit("changeUnReadMsgNum", {
      userId: this.userId,
    });
  },
  destroyed() {
    // 取消接收ws消息
    this.$root.$off("pushNewMessage", this.pushNewMessage);
  },
  filters: {
    dateFormat(v) {
      if (!v) {
        return;
      }
      // 如果是昨天发的消息,左侧列表中展示的时间只显示月、日
      if (v.substr(0, 10) !== dateFormat(new Date()).substr(0, 10)) {
        return dateFormat(v, "MM-DD");
      }
      // 如果是今天发的消息,左侧列表中展示的时间显示时、分
      return dateFormat(v, "HH:mm");
    },
  },
  methods: {
    delUserByUserId(item) {
      this.$root.$emit("loadUserList", {
        userId:item.userId,
      });
    },
    // 获取历史记录
    getSelectedUserMessageList() {
      API["storeHouseChatMsgHistory"]({
        sendId: `${this.sendId}`,
        receiveId: this.userId,
        pageSize: "100",
        currentPage: "1",
      }).then((res) => {
        const { data } = res;
        this.content = data.list.reverse();
        this.scrollAuto();
      });
    },
    pushNewMessage(news) {
      if (news.code == 0) {
        console.log(11112, news);
        for (var i = 0; i < this.content.length; i++) {
          this.content[i].readState = 1;
        }
        return;
      }
      // 接收到的新消息
      if (news.sendId != this.userId) return; // 这里判断id对不对再推数据
      this.content.push({
        flag: 0,
        name: news.chatMsg,
        readState: "0",
      });
      this.scrollAuto();
      // 这里是为了做消除用户列表中的未读,也就是未读变为已读,告诉后端消息已读
      API["msgRead"]({
        sendId: `${this.sendId}`,
        receiveId: `${this.userId}`,
      });
      // 更改前端页面上的数据,不再提示几条未读
      this.$root.$emit("changeUnReadMsgNum", {
        userId: news.sendId,
      });

      // if(news.code ==0){
      // 		for(var i=0;i<this.content.length;i++){
      // 			this.content[i].readState=1
      // 		}
      // }
    },
    // 按enter键(也就是要发送消息)
    submit() {
      if (!this.text) {
        return;
      }
      this.content.push({
        flag: 1,
        name: this.text,
        readState: "0",
      });
      // 修改左侧列表上展示的最后一条消息和时间
      this.$root.$emit("changeLast", {
        lastMsg: this.text,
        lastMsgTime: dateFormat(Date.now()),
        userId: this.userId,
      });
      this.$root.$talkRoomWs.send(
        JSON.stringify({
          linkType: "msg",
          sendId: `${this.sendId}`,
          userId: this.sendId,
          receiveId: this.userId,
          msgKey: `${this.sendId}_${this.userId}`,
          chatMsg: this.text,
          readState: "0",
        })
      );
      this.text = null;
      this.scrollAuto();
    },
    // 这里是点击左侧成员列表时执行的函数
    CreateConnection(item) {
      if (this.userId === item.userId) return;
      this.contactsName = item.nickName;
      this.text = "";
      this.content = [];
      this.userId = item.userId;
      this.getSelectedUserMessageList();
      this.$root.$emit("changeUnReadMsgNum", item);
      API["msgRead"]({
        sendId: `${this.sendId}`,
        receiveId: `${this.userId}`,
      });
      // 点击左侧列表时,聊天框中的头像也要切换
      for (var i = 0; i < this.userList.length; i++) {
        if (this.userList[i].userId == item.userId) {
          this.heardUrl = this.userList[i].heardUrl;
        }
      }
    },
    scrollAuto() {
      this.$nextTick(() => {
        this.$refs["reference"].scrollTop =
          this.$refs["reference"].scrollHeight;
      });
 
    },
  },
};
</script>

<style lang="scss" scoped>
h3 {
  text-align: center;
  height: 40px;
  line-height: 40px;
  border-bottom: 0.5px solid #ccc;
  font-size: 16px;
  font-weight: 300;
}
.box {
  margin-top: 150px;
  margin-left: 28%;
  width: 900px;
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
  .min {
    display: flex;
    .contactList {
      list-style: none;
      overflow: auto;
      height: 600px;
      width: 260px;
      background: #4387f6;
      color: azure;
      padding: 0px 5px;
      padding-left: 5px;

      .red {
        background: #5d9aff;
        border-radius: 10px;
      }

      li {
        cursor: pointer;
        text-align: left;
        box-sizing: border-box;
        padding: 14px 0px;
        padding-left: 5px;
        height: 65px;
      }

      .leftLi {
        @extend .min;
        img {
          height: 40px;
          display: block;
        }
        .ListItem {
          @extend .min;
          padding-left: 5px;
          flex-direction: column;
        }

        div :first-child {
          height: 30px;
          line-height: 20px;
          span {
            color: #bed7ff;
            font-size: 12px;
            margin-left: 15px;
          }
        }

        div :last-child {
          line-height: 0px;
          color: #bed7ff;
          font-size: 12px;
          span {
            text-align: center;
            border-radius: 3px;
          }
        }
      }
    }
    .chat {
      background: #f3f3f3;
      width: 700px;
      height: 600px;
      @extend .min;
      flex-direction: column;
      .content {
        overflow: auto;
        height: 500px;
        span {
          display: inline-block;
          padding: 0px 5px;
          line-height: 28px;
          @mixin name($px) {
            transition: 0.5s;
            opacity: 0;
            transform: translate($px);
          }
          .time {
            @include name(-20px);
          }
          .times {
            @include name(20px);
          }
        }

        span:hover .time {
          opacity: 1;
          transform: translate(0px);
        }
        span:hover .times {
          opacity: 1;
          transform: translate(0px);
        }

        @mixin Bubble($color) {
          background: $color;
          border-radius: 5px;
          border-bottom-left-radius: 20px;
        }

        .i {
          color: #f3f3f3;
          @include Bubble(#5d9aff);
          max-width: 65%;
          text-align: left;
          word-wrap: break-word;
          word-break: break-all;
          overflow: hidden;
          padding: 8px 5px;
        }
        .he {
          @include Bubble(#e9e9e9);
          max-width: 65%;
          word-wrap: break-word;
          word-break: break-all;
          overflow: hidden;
          padding: 8px 5px;
        }
      }
      .input {
        height: 180px;
        border-top: 0.5px solid #f3f3f3;
        textarea {
          width: 100%;
          border: 0px;
          resize: none;
        }
      }
    }
  }
}

ul > li {
  width: 100px;
  text-align: center;
  width: 100%;
  margin-top: 3px;
}

img {
  height: 45px;
  vertical-align: top;
  border-radius: 50%;
}
.content li > span {
  position: relative;
}
.content li {
  margin-top: 15px;
}
.readOrUnread {
  font-size: 12px;
  margin-right: 47px;
  line-height: 12px;
}
.ellipsis {
  width: 100px;
  height: 12px;
  line-height: 12px !important;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nickName {
  display: inline-block;
  width: 90px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.leftLi {
  position: relative;
}
.delete {
  position: absolute;
  bottom: 3px;
  right: 45px;
  color: #fff;
}
</style>

message.js

import * as API from "@/api/systemApplication";
export default {
    data() {
        return {
            sendId: 1, // todo 客服id,默认为1
            scoket: null,
            userList: [], //联系人列表
            talkRoomVisible: false,
        }
    },
    computed: {
        // 未读消息的数量
        unReadMessageNum() {
            let unReadNum = 0;
            for (let i = 0; i < this.userList.length; i++) {
                const e = this.userList[i];
                // 注意这里的unReadMsgNum可能不是数字
                if (e.unReadMsgNum && typeof e.unReadMsgNum == 'number') {
                    unReadNum += e.unReadMsgNum
                }
            }
            return unReadNum
        }
    },
    mounted() {
        // 初始化ws
        this.initWs();
        // 获取左侧用户列表
        this.getList();
        // 如果聊天窗口是开启状态,按esc键,则关闭聊天窗口
        this.initCloseTalkRoom()
        // 修改前端页面上左侧列表展示的未读条数
        this.$root.$on("changeUnReadMsgNum", this.changeUnReadMsgNum);
        // 修改左侧列表上展示的最后一条消息和时间
        this.$root.$on("changeLast", this.changeLast);
        // 删除聊天室左侧列表中的项,再重新加载用户列表
        this.$root.$on("loadUserList", this.loadUserList);
    },
    methods: {
        initWs() {
            // let url = this.URL;
            // this.$root.$talkRoomWs = new WebSocket("ws://192.168.1.169:9019/fmis-api/ws/asset");
            let url = window.ROOT;
            this.$root.$talkRoomWs =  new WebSocket(`${url.replace("http", "ws")}/ws/asset`);
            let ws = this.$root.$talkRoomWs;
            ws.onopen = (e) => {
                console.log(`WebSocket 连接状态: ${ws.readyState}`);
            };
            ws.onmessage = (data) => {
                try {
                    let news = JSON.parse(data.data)
                    // 用新信息覆盖旧消息
                    if (news.unReadNumLst) {
                        for (let i = 0; i < news.unReadNumLst.length; i++) {
                            const e = news.unReadNumLst[i];
                            let index = this.userList.findIndex(item => item.userId == e.userId)
                            if (index > -1) {
                                // this.userList[index].lastMsg = e.lastMsg
                                // this.userList[index].lastMsgTime = e.lastMsgTime
                                // this.userList[index].unReadMsgNum = e.unReadMsgNum
                                Object.assign(this.userList[index], e)
                            } else {
                                // todo 新增的逻辑
                                this.userList.push(e)
                            }
                        }
                    }

                    this.$root.$emit('pushNewMessage', news)

                } catch (err) {
                    // console.log(err);
                }
            };

            ws.onclose = (data) => {
                console.log("WebSocket连接已关闭");
            };
            setTimeout(() => {
                ws.send(
                    JSON.stringify({
                        linkType: "bind",
                        sendId: this.sendId,
                    })
                );
            }, 500);
        },
        // 获取左侧的用户列表
        getList() {
            API["storeHouseWsUserList"]({
                sendId: this.sendId,
                userId: this.sendId,
            }).then((res) => {
                const { data } = res;
                this.userList = data;
            });
        },
        // 如果聊天窗口是开启状态,按esc键,则关闭聊天窗口
        initCloseTalkRoom() {
            window["onkeydown"] = (e) => {
                if (e.keyCode === 27 && this.talkRoomVisible) {
                    this.talkRoomVisible = false
                }
            };
        },
        // 修改前端页面上左侧列表展示的未读条数
        changeUnReadMsgNum(item) {
            let index = this.userList.findIndex((e) => e.userId == item.userId);
            console.log(index);
            if (index > -1) {
                this.userList[index].unReadMsgNum = 0;
            }
            console.log(this.userList[index].unReadMsgNum);
        },
        // 修改左侧列表上展示的最后一条消息和时间
        changeLast(obj) {
            this.userList.find((item) => {
                if (item.userId === obj.userId) {
                    item.lastMsg = obj.lastMsg;
                    item.lastMsgTime = obj.lastMsgTime;
                }
            });
        },
        loadUserList(item) {
            console.log(item,"item")
            let postData= {
                userId:item.userId
            }
            API["delUserByUserId"](postData).then((res) => {
                console.log(res);
                if (res.code) {
                    this.$message({
                        message: res.message,
                        type: "success",
                    });
                    this.getList()
                }
            });
        }
    }
}

systemApplication.js


// 获取列表
export function storeHouseWsUserList(data) {
  return request({
    url: '/chat/userList',
    method: 'post',
    data
  })
}
// 获取历史连天记录
export function storeHouseChatMsgHistory(data) {
  return request({
    url: '/chat/msgHistory',
    method: 'post',
    data
  })
}
// 已读未读
export function msgRead(data) {
  return request({
    url: '/chat/msgRead',
    method: 'post',
    data
  })
}

// 删除左侧列表中的项

export function delUserByUserId(data) {
  return request({
    url: '/chat/delUserByUserId',
    method: 'post',
    data
  })
}
2.后端代码

WebSocketConfig

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter () {
        return new ServerEndpointExporter();
    }
}

WebSocketServer

import com.alibaba.fastjson.JSONObject;
import com.jinmdz.fmis.api.api.model.storehouse.chat.ChatMsg;
import com.jinmdz.fmis.api.global.CacheManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author lvyq
 * @version 1.0
 * @description: TODO
 * @date 2021/8/17 16:30
 */
@ServerEndpoint(value = "/ws/asset")
@Component
public class WebSocketServer {

    private static ChatService  chatService;
    @Autowired
    public  void setChatService(ChatService chatService) {
        WebSocketServer.chatService = chatService;
    }

    @PostConstruct
    public void init() {
        System.out.println("websocket 加载");
    }
    private static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
    private static final AtomicInteger OnlineCount = new AtomicInteger(0);
    // concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
    private static CopyOnWriteArraySet<Session> SessionSet = new CopyOnWriteArraySet<Session>();
    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        SessionSet.add(session);
        int cnt = OnlineCount.incrementAndGet();
        log.info("有连接加入,当前连接数为:{}", cnt);
        SendMessage(session, "连接成功");
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session) {
        SessionSet.remove(session);
        int cnt = OnlineCount.decrementAndGet();
        log.info("有连接关闭,当前连接数为:{}", cnt);
    }

    /**
     * 收到客户端消息后调用的方法
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("来自客户端的消息:{}",message);
        try{
            JSONObject jsonObject = JSONObject.parseObject(message);
            String linkType = jsonObject.getString("linkType");
            String sendId = jsonObject.getString("sendId");
            if (linkType.equals("bind")){
                CacheManager.set("bind_"+sendId,session);
                SendMessage(session, "连接成功");
            }else if (linkType.equals("msg")){
                //聊天
                ChatMsg msg = new ChatMsg();
                //发消息
                String chatMsg = jsonObject.getString("chatMsg");
                String receiveId = jsonObject.getString("receiveId");
                msg.setChatMsg(chatMsg);
                msg.setSendId(sendId);
                msg.setMsgKey(sendId+"_"+receiveId);
                msg.setReceiveId(receiveId);
                msg.setReadState("0");
                chatService.sendOne(msg);}
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 出现错误
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误:{},Session ID: {}",error.getMessage(),session.getId());
        error.printStackTrace();
    }

    /**
     * 发送消息,实践表明,每次浏览器刷新,session会发生变化。
     * @param session
     * @param message
     */
    public static void SendMessage(Session session, String message) {
        try {
            //session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)",message,session.getId()));
            log.info("sessionID="+session.getId());
            session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            log.error("发送消息出错:{}", e.getMessage());
            e.printStackTrace();
        }
    }


    /**
     * 指定Session发送消息
     * @param sessionId
     * @param message
     * @throws IOException
     */
    public static void SendMessageById(String message,String sessionId) throws IOException {
        Session session = null;
        for (Session s : SessionSet) {
            if(s.getId().equals(sessionId)){
                session = s;
                break;
            }
        }
        if(session!=null){
            SendMessage(session, message);
        }
        else{
            log.warn("没有找到你指定ID的会话:{}",sessionId);
        }
    }
    /**
     * @description: 指定发送
     * @author: lvyq
     * @date: 2021/9/24 11:30
     * @version 1.0
     */
    public static void SendMessageByRecId(String message,String receiveId) throws IOException {
        Session session=  CacheManager.get("bind_"+receiveId);
        String sessionId = "";
        if (session!=null){
            sessionId=session.getId();
        }
        for (Session s : SessionSet) {
            if(s.getId().equals(sessionId)){
                session = s;
                break;
            }
        }
        if(session!=null){
            SendMessage(session, message);
        }
        else{
            log.warn("没有找到你指定ID的会话:{}",sessionId);
        }
    }

ChatController

import com.jinmdz.fmis.api.api.model.storehouse.chat.ChatMsg;
import com.jinmdz.fmis.api.api.model.storehouse.chat.SysUser;
import com.jinmdz.fmis.api.api.service.ChatService;
import com.jinmdz.fmis.api.api.service.WebSocketServer;
import com.jinmdz.fmis.api.base.BaseController;
import com.jinmdz.fmis.core.base.BaseResult;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.IOException;

/**
 * @author lvyq
 * @version 1.0
 * @description: TODO-消息通信
 * @date 2021/9/26 14:50
 */
@RestController
@RequestMapping("/chat")
public class ChatController extends BaseController {

    @Resource
    private ChatService chatService;

    @Resource
    private WebSocketServer webSocketServer;

    /**
     * @description: 消息记录
     * @author: lvyq
     * @date: 2021/9/26 15:19
     * @version 1.0
     */
    @PostMapping("/msgHistory")
   public BaseResult msgHistory(@RequestBody ChatMsg data) throws IOException {
        return  chatService.msgHistory(data);
   }

   /**
    * @description: 消息已读
    * @author: lvyq
    * @date: 2021/9/26 16:27
    * @version 1.0
    */
   @PostMapping("/msgRead")
   private BaseResult msgRead(@RequestBody ChatMsg data) throws IOException {
       return  chatService.msgRead(data);
   }

   /**
    * @description: 全体发送
    * @author: lvyq
    * @date: 2021/11/11 13:38
    * @version 1.0
    */
   @GetMapping("/sendAll/{msg}")
    public void sendAll(@PathVariable("msg") String msg) throws IOException {
       WebSocketServer.BroadCastInfo(msg);
   }

    /**
     * @description: 发消息给某人
     * @author: lvyq
     * @date: 2021/9/24 11:15
     * @version 1.0
     */
    @PostMapping("/sendOne")
    public void sendOne(@RequestBody ChatMsg data) throws IOException {
        chatService.sendOne(data);
    }

    /**
     * @description: 获取用户列表
     * @author: lvyq
     * @date: 2021/9/24 13:43
     * @version 1.0
     */

    @PostMapping("/userList")
    private BaseResult userList(@RequestBody SysUser data){
        return chatService.userListDev(data);
    }

    /**
     * @description: 根据userId删除用户
     * @author: lvyq
     * @date: 2021/11/19 13:29
     * @version 1.0
     */
    @PostMapping("/delUserByUserId")
    public BaseResult delUserByUserId(@RequestBody SysUser data){
        return chatService.delUserByUserId(data.getUserId());
    }

}

ChatService

import com.alibaba.fastjson.JSONObject;
import com.jinmdz.fmis.api.api.model.storehouse.chat.ChatMsg;
import com.jinmdz.fmis.api.api.model.storehouse.chat.SysUser;
import com.jinmdz.fmis.api.api.service.repository.SysUserRepository;
import com.jinmdz.fmis.api.base.BaseService;
import com.jinmdz.fmis.api.model.system.UserItem;
import com.jinmdz.fmis.core.base.BaseResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author lvyq
 * @version 1.0
 * @description: TODO
 * @date 2021/9/24 10:06
 */
@Service
public class ChatService extends BaseService {

    @Resource
    private MongoTemplate mongoTemplate;

    /**
     * @description: 获取用户列表
     * @author: lvyq
     * @date: 2021/9/24 10:12
     * @version 1.0
     */
    
    public BaseResult userList(UserItem userItem, SysUser data) {
        String receiveId = userItem.getId().toString();
        List<SysUser> sysUserList = userLst(receiveId);
        return successData(sysUserList);
    }

    /**
     * @description: 发消息给某人
     * @author: lvyq
     * @date: 2021/9/24 11:16
     * @version 1.0
     */
    public void sendOne(ChatMsg data) throws IOException {
        mongoTemplate.save(data);
        //TODO 更新消息  接收者_发送者
        String msg_key = data.getReceiveId()+"_"+data.getSendId();
        String receiveId = data.getReceiveId();
        HashMap<String,Object> map = new HashMap<>();
        JSONObject mav = new JSONObject();
        Query queryMsg = new Query().addCriteria(Criteria.where("readState").is("0").and("msgKey").is(msg_key));
        Update update = new Update().set("readState","1");
        //批量修改
        mongoTemplate.updateMulti(queryMsg,update,ChatMsg.class,"chat_msg");
        mav.put("sendId",data.getSendId());
        mav.put("chatMsg",data.getChatMsg());
        mav.put("unReadNumLst",userLst(receiveId));
        WebSocketServer.SendMessageByRecId(mav.toJSONString(),receiveId);
    }


    /**
     * @description: 消息列表信息-unLogin
     * @author: lvyq
     * @date: 2021/9/26 17:50
     * @version 1.0
     */
    public BaseResult userListDev(SysUser data) {
        String receiveId = data.getUserId();
        List<SysUser> sysUserList = userLst(receiveId);
        return successData(sysUserList);
    }

    /**
     * @description: 消息记录
     * @author: lvyq
     * @date: 2021/9/26 15:20
     * @version 1.0
     */
    
    public BaseResult msgHistory(ChatMsg data) throws IOException {
        int page = data.getCurrentPage();
        if (page<0){
            page=1;
        }
        page=page-1;
        int size = data.getPageSize();
        if (size<0){
            size=20;
        }
        if (StringUtils.isNotEmpty(data.getSendId()) && StringUtils.isNotEmpty(data.getReceiveId())){
            String msgKeyOne= data.getSendId()+"_"+data.getReceiveId();
            String msgKeyTwo = data.getReceiveId()+"_"+data.getSendId();
            Criteria orCri1 = new Criteria();
            Criteria orCri2 = new Criteria();
            orCri1.andOperator(Criteria.where("msgKey").is(msgKeyOne));
            orCri2.andOperator(Criteria.where("msgKey").is(msgKeyTwo));
            Query query = new Query().addCriteria(new Criteria().orOperator(orCri1,orCri2));
            long total = mongoTemplate.count(query,ChatMsg.class);
            Pageable pageable = PageRequest.of(page,size, Sort.by(Sort.Direction.DESC,"createTime"));
            query.with(pageable);
            List<ChatMsg> list = mongoTemplate.find(query,ChatMsg.class);
            HashMap<String, Object> map = new HashMap<>();
            if (list != null) {
                for (ChatMsg chatMsg:list){
                    chatMsg.setName(chatMsg.getChatMsg());
                    if (chatMsg.getSendId().equals(data.getSendId())){
                            chatMsg.setFlag(1);
                    }else {
                        chatMsg.setFlag(0);
                    }
                }
                Map<String, Object> pageMap = new HashMap<>();
                map.put("list", list);
                pageMap.put("total", total);
                pageMap.put("pageSize", data.getPageSize());
                pageMap.put("currentPage", data.getCurrentPage());
                map.put("pager", pageMap);
            }
            Query queryMsg = new Query().addCriteria(Criteria.where("readState").is("0").and("sendId").is(data.getReceiveId()).and("receiveId").is(data.getSendId()));
            Update update = new Update().set("readState","1");
            mongoTemplate.updateMulti(queryMsg,update,ChatMsg.class,"chat_msg");
           //消息已读通知
            /*JSONObject mav = new JSONObject();
            mav.put("state",true);
            mav.put("msg","消息已读");
            WebSocketServer.SendMessageByRecId(mav.toJSONString(),data.getReceiveId());*/
            return successData(map);
        }else {
            return failure("非法参数");
        }

    }

    /**
     * @description: 将消息修改为已读
     * @author: lvyq
     * @date: 2021/9/26 16:27
     * @version 1.0
     */
    
    public BaseResult msgRead(ChatMsg data) throws IOException {
        Query query = new Query().addCriteria(Criteria.where("readState").is("0").and("sendId").is(data.getReceiveId()).and("receiveId").is(data.getSendId()));
        Update update = new Update().set("readState","1");
        mongoTemplate.updateMulti(query,update,ChatMsg.class,"chat_msg");
        //消息已读通知
        JSONObject mav = new JSONObject();
        mav.put("state",true);
        mav.put("msg","消息已读");
        mav.put("code",0);
        WebSocketServer.SendMessageByRecId(mav.toJSONString(),data.getReceiveId());
        return success("操作成功");

    }

    /**
     * @description: 获取集合
     * @author: lvyq
     * @date: 2021/9/26 17:46
     * @version 1.0
     */
    private  List<SysUser> userLst(String receiveId){
        List<SysUser> sysUserList = mongoTemplate.findAll(SysUser.class);
        for (SysUser sysUser:sysUserList){
            String sendId = sysUser.getUserId();
            String msgKey = sendId+"_"+receiveId;
            if (sendId.equals(receiveId)){
                continue;
            }
            //最新一条记录
            String msgKeyOne= sendId+"_"+receiveId;
            String msgKeyTwo = receiveId+"_"+sendId;
            Criteria orCri1 = new Criteria();
            Criteria orCri2 = new Criteria();
            orCri1.andOperator(Criteria.where("msgKey").is(msgKeyOne));
            orCri2.andOperator(Criteria.where("msgKey").is(msgKeyTwo));
            Query msgQuery = new Query().addCriteria(new Criteria().orOperator(orCri1,orCri2));
            Pageable pageable = PageRequest.of(0,1, Sort.by(Sort.Direction.DESC,"createTime"));
            msgQuery.with(pageable);
            List<ChatMsg> list = mongoTemplate.find(msgQuery,ChatMsg.class);
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            if (list.size()>0){
                  sysUser.setLastMsg(list.get(0).getChatMsg());
                  sysUser.setLastMsgTime(simpleDateFormat.format(list.get(0).getCreateTime()));
            }else {
                sysUser.setLastMsg("");
                sysUser.setLastMsgTime("");
            }
            Query query = new Query().addCriteria(Criteria.where("readState").is("0").and("msgKey").is(msgKey));
            long unReadNum = mongoTemplate.count(query, ChatMsg.class);
            sysUser.setUnReadMsgNum(unReadNum);
        }
        return sysUserList;
    }

    /**
     * @description:时间转换
     * @author: lvyq
     * @date: 2021/9/27 16:48
     * @version 1.0
     */
    private String changeTime(Date createTime) {
        Date date = new Date();
        SimpleDateFormat formatDay = new SimpleDateFormat("MM-dd");
        if (formatDay.format(date).equals(formatDay.format(createTime))){
            SimpleDateFormat formatMin = new SimpleDateFormat("HH:mm");
            return formatMin.format(createTime);
        }else {
            return formatDay.format(createTime);
        }

    }

    /**
     * @description: 根据userId删除用户
     * @author: lvyq
     * @date: 2021/11/19 13:29
     * @version 1.0
     */
    
    public BaseResult delUserByUserId(String userId) {
        Query query = new Query().addCriteria(Criteria.where("userId").is(userId));
        mongoTemplate.findAndRemove(query,SysUser.class);
        return success("操作成功");
    }
}

ChatMsg

import com.fasterxml.jackson.annotation.JsonFormat;
import com.jinmdz.fmis.core.base.BasePageData;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;

import java.util.Date;

/**
 * @author lvyq
 * @version 1.0
 * @description: 消息通信
 * @date 2021/9/24 9:00
 */
@Data
@Document("chat_msg")
public class ChatMsg extends BasePageData {
    @Id
    private String id;

    /**
     * 创建时间
     */
    @JsonFormat(pattern = yyyy_MM_dd_HH_mm_ss)
    private Date createTime=new Date();

    /**
     * 通信key(sendId_receiveId)
     */
    private String msgKey;

    /**
     * 通信消息
     */
    private String chatMsg;

    /**
     * 发送id
     */
    private String sendId;

    /**
     * 接收id
     */
    private String receiveId;

    /**
     * 查看状态 0未看 1已看
     */
    private String readState;

    /**
     *1为我    0 为他
     */
    @Transient
    private Integer flag;

    @Transient
    private String  name;
}

SysUser

import com.jinmdz.fmis.core.base.BasePageData;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;

/**
 * @author lvyq
 * @version 1.0
 * @description: TODO
 * @date 2021/9/24 9:14
 */
@Data
@Document("sys_user")
public class SysUser extends BasePageData {
    @Id
    private String id;
    /**
     * 用户id
     */
    private String userId;

    /**
     * 昵称
     */
    private String nickName;


    /**
     * 头像地址
     */

    private String heardUrl;

    /**
         * 性别
     */
    private Integer sex;

    /**
     * 登录状态 0未登录 1已登录
     */
    private Integer loginState;

    /**
     * 用户发给自己且未读的消息数
     */
    @Transient
    private long unReadMsgNum;

    /**
     * 最新一条消息
     */
    @Transient
    private String lastMsg;

    /**
     * 最新一条消息时间
     */
    @Transient
    private String lastMsgTime;

}

2.移动端

1.前端代码:

说明:userid在登录系统时存储在了cookie中

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
    <title>问题解答</title>
    <script src="../../common/js/GBCookie.js"></script>

    <style type="text/css">
        body {
            overflow: hidden;
        }

        h3 {
            text-align: center;
            height: 40px;
            line-height: 40px;
            border-bottom: 0.5px solid #ccc;
            font-size: 16px;
            font-weight: 300;
        }

        .chat {
            background: #f3f3f3;
            /*width: 700px;*/
            /*height: 600px;*/
            width: 100vw;
            height: 100vh;
        }

        .chat > h3 {
            height: 5vh;
            line-height: 5vh;
        }

        .content {
            overflow: auto;
            /*height: 500px;*/
            height: 75vh;
        }

        ul {
            list-style: none;
        }

        ul > li {
            width: 100px;
            text-align: center;
            line-height: 50px;
            width: 100%;
            margin-top: 3px;
        }

        img {
            height: 45px;
            vertical-align: bottom;
            border-radius: 50%;
        }

        .input {
            height: 180px;
            border-top: 0.5px solid #f3f3f3;
        }

        textarea {
            width: 100%;
            border: 0px;
            resize: none;
            height: 20vh;
        }

        span {
            display: inline-block;
            padding: 0px 5px;
            height: 37px;
            /*line-height: 37px;*/
            line-height: 30px;

        }

        span:hover .times {
            opacity: 1;
            transform: translate(0px);
        }

        span:hover .time {
            opacity: 1;
            transform: translate(0px);
        }

        .times {
            -webkit-transition: 0.5s;
            transition: 0.5s;
            opacity: 0;
            -webkit-transform: translate(20px);
            transform: translate(20px);
        }

        .time {
            -webkit-transition: 0.5s;
            transition: 0.5s;
            opacity: 0;
            -webkit-transform: translate(-20px);
            transform: translate(-20px);
        }

        .he {
            background: #e9e9e9;
            border-radius: 5px;
            border-bottom-right-radius: 15px;

        }

        .i {
            color: #f3f3f3;
            background: #5d9aff;
            border-radius: 5px;
            border-bottom-left-radius: 14px;
        }

        .state {
            font-size: 12px;
        }

        .readOrUnread {
            /*position: absolute;*/
            /*right: 55px;*/
            /*bottom: -39px;*/
            /*font-size: 12px;*/
            margin-right: 55px;
        }

        .content li > span {
            position: relative;
        }

        .content li {
            margin-top: 15px;
        }

        li .i,
        li .he {
            max-width: 50vw;
            word-wrap: break-word;
            word-break: break-all;
            text-align: left;
            height: auto;
            padding: 7px 5px;
        }
    </style>
    <script src="../../common/vue/vue.min.js"></script>
    <script src="../../common/axios/axios.min.js"></script>
    <link rel="stylesheet" href="../../common/css/base.css">
</head>
<body>

<div id="app">
    <div class="chat"><h3>客服<span class="state"></span></h3>
        <div class="content" ref="reference">
            <ul style="color: black">
                <li v-for="item in content"
                    :key="item.id"
                    :style="item.flag ? 'text-align: right;' : 'text-align: left;'">
                    <span>
                      <img v-show="!item.flag" src="../img/icon/01.png" />
                      <span class="times" v-show="item.flag">{{
                              item.createTime
                                      ? item.createTime.substr(11, item.createTime.length)
                                      : ""
                          }}</span>
                    <span :class="item.flag ? 'i' : 'he'">{{ item.name }}</span>
                    <span class="time" v-show="!item.flag">{{
                            item.createTime
                                    ? item.createTime.substr(11, item.createTime.length)
                                    : ""}}
                    </span>
                  <img
                          v-show="item.flag"
                          :src="heardUrl"
                  />
                  <div class="readOrUnread" v-if="item.flag">
                    {{ item.readState == "1" ? "已读" : "未读" }}
                  </div>
                </span>
                </li>
            </ul>
        </div>
        <div class="input">
            <textarea
                    cols="50"
                    rows="10"
                    v-model="text"
                    @keydown.enter="submit"
            ></textarea>
        </div>
    </div>
</div>

<script type="text/javascript">
    var app = new Vue({
        el: "#app",
        data() {
            return {
                text: "",
                content: [],
                socket: "",
                userId: "",
                openId: "",
                heardUrl: "",
                webUrl: ""
            }
        },
        created() {
            this.openId = getCookie('openId');
        },
        mounted() {
            this.getWebUrl()
            this.getSysUserInfoByUserId()
        },
        methods: {
            submit() {
                if (!this.text) {
                    return;
                }
                this.content.push({
                    flag: 1,
                    name: this.text,
                });
                this.socket.send(
                    JSON.stringify({
                        linkType: "msg",
                        sendId: this.openId,
                        userId: this.openId,
                        receiveId: 1,
                        msgKey: this.openId + '_1',
                        chatMsg: this.text,
                        readState: 0,
                    })
                );
                this.text = null;
                this.scrollAuto();
            },
            scrollAuto() {
                this.$nextTick(() => {
                    this.$refs["reference"].scrollTop = this.$refs["reference"].scrollHeight
                });
            },
            history() {
                axios({
                    method: "post",
                    url: "../../../chat/msgHistory",
                    data: {
                        receiveId: 1,
                        sendId: this.openId,
                        pageSize: "20",
                        currentPage: "1",
                    }
                }).then((res) => {
                    if (res) {
                        const {data} = res;
                        this.content = data.data.list.reverse();
                        this.scrollAuto();
                    }
                })
                this.msgRead()
            },
            msgRead() {
                let url = `http://${this.webUrl}/chat/msgRead`
                console.log(url, "url")
                axios({
                    method: "post",
                    url: url,
                    data: {
                        sendId: this.openId,
                        receiveId: `1`,
                    }
                })
            },
            getWebUrl() {
                axios({
                    method: "POST",
                    url: "../../../chat/getWebUrl",
                }).then((res) => {
                    if (res) {
                        const {data} = res
                        this.webUrl = data.webUrl
                        this.history()
                        this.initWs()
                    }

                })
            },
            initWs() {
                if (typeof (WebSocket) == "undefined") {
                    console.log("遗憾:您的浏览器不支持WebSocket");
                } else {
                    console.log("恭喜:您的浏览器支持WebSocket");
                    this.socket = new WebSocket(`ws://${this.webUrl}/ws/asset`);
                    //连接打开事件
                    this.socket.onopen = function () {
                        console.log("Socket 已打开");
                        //session与用户id绑定
                        this.socket.send("{\"linkType\":\"bind\",\"sendId\":\""${this.sendId}"\"}  ");
                    };
                    //收到消息事件
                    this.socket.onmessage = (data) => {
                        console.log(data)
                        if (data.data !== '连接成功') {
                            let news = JSON.parse(data.data);
                            this.content.push({
                                flag: 0,
                                name: news.chatMsg,
                            });
                            //对方接收到消息之后,把未读改为已读
                            if (news.code == 0) {
                                this.content.pop()
                                for (var i = 0; i < this.content.length; i++) {
                                    this.content[i].readState = 1
                                }
                            }
                            this.msgRead()
                            this.scrollAuto();
                        }

                    };
                    // socket.
                    //连接关闭事件
                    this.socket.onclose = function () {
                        console.log("Socket已关闭");
                    };
                    //发生了错误事件
                    this.socket.onerror = function () {
                        alert("Socket发生了错误");
                    }

                    //窗口关闭时,关闭连接
                    window.unload = function () {
                        this.socket.close();
                    };
                }
                setTimeout(() => {
                    this.socket.send(
                        JSON.stringify({
                            linkType: "bind",
                            sendId: this.openId,
                        })
                    );
                }, 500);
            },
            //通过openId获取用户头像信息
            getSysUserInfoByUserId() {
                axios({
                    method: "GET",
                    url: "../../../chat/getSysUserInfoByUserId",
                    params: {
                        "openId": this.openId
                    }
                }).then((res) => {
                    if (res.data.success) {
                        if (res.data.data) {
                            this.heardUrl = res.data.data.heardUrl
                            console.log(this.heardUrl)
                        }

                    }
                })
            }
        }
    })
    
</script>
</body>
</html>
 

GBCookie.js

function setCookie(c_name, value, expiredays) {
    var exdate = new Date()
    exdate.setDate(exdate.getDate() + expiredays)
    document.cookie = c_name + "=" + escape(value) +
        ((expiredays == null) ? "" : ";expires=" + exdate.toGMTString())
}

//取回cookie
function getCookie(c_name) {
    if (document.cookie.length > 0) {
        c_start = document.cookie.indexOf(c_name + "=")`在这里插入代码片`
        if (c_start != -1) {
            c_start = c_start + c_name.length + 1
            c_end = document.cookie.indexOf(";", c_start)
            if (c_end == -1) c_end = document.cookie.length
            return unescape(document.cookie.substring(c_start, c_end))
        }
    }
    return ""
}

2.后端代码

引入mongodb依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>

yml配置

#mongodb 配置
spring:
  data:
    mongodb:
      host: 192.168.1.125
      port: 27017
      database: cangzhou
      # 客服消息地址
websocket:
  url: 192.168.1.209:9019/fmis

WxChatController

import com.alibaba.fastjson.JSONObject;
import com.jinmdz.common.response.CommonCode;
import com.jinmdz.entity.wechat.SysUser;
import com.jinmdz.entity.wechat.WeChatResult;
import com.jinmdz.model.chat.ChatMsg;
import com.jinmdz.service.ChatService;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * @author lvyq
 * @version 1.0
 * @description: TODO
 * @date 2021/11/12 13:18
 */
@RestController
@RequestMapping("/chat")
public class WxChatController {
    @Resource
    public MongoTemplate mongoTemplate;

    @Resource
    private ChatService chatService;

    /**
     * @description: 添加用户-测试
     * @author: lvyq
     * @date: 2021/11/18 16:08
     * @version 1.0
     */

    @PostMapping("/saveUser")
    public WeChatResult saveUser(@RequestBody SysUser user){
        Query query = new Query().addCriteria(Criteria.where("userId").is(user.getUserId()));
        Update update = new Update().set("nickName",user.getNickName()).set("heardUrl",user.getHeardUrl());
        SysUser sysUser=  mongoTemplate.findOne(query,SysUser.class);
        if (sysUser!=null){
            mongoTemplate.updateFirst(query,update,SysUser.class);
        }else {
            mongoTemplate.insert(user);

        }
        return new WeChatResult(CommonCode.SUCCESS);
    }
    /**
     * @description:  消息记录
     * @author: lvyq
     * @date: 2021/11/12 13:18
     * @version 1.0
     */
    @PostMapping("/msgHistory")
    public JSONObject msgHistory(@RequestBody ChatMsg data){
        return  chatService.msgHistory(data);
    }

    

    /**
     * @description: 获取ws的地址
     * @author: lvyq
     * @date: 2021/11/16 14:45
     * @version 1.0
     */
    
    @PostMapping("/getWebUrl")
    public JSONObject getWebUrl(){
        return chatService.getWebUrl();
    }
       
     /**
     * @description: 根据用户openId获取用户基本信息
     * @author: lvyq
     * @date: 2021/11/18 16:27
     * @version 1.0
     */
    
    @GetMapping("/getSysUserInfoByUserId")
    public WeChatResult getSysUserInfoByUserId(String openId){
        if (StringUtils.isEmpty(openId)){
            return new WeChatResult(CommonCode.INVALID_PARAM);
        }
        Query query = new Query().addCriteria(Criteria.where("userId").is(openId));
        SysUser sysUser = mongoTemplate.findOne(query,SysUser.class);
        return new WeChatResult(sysUser);
    }

}

ChatService

import com.alibaba.fastjson.JSONObject;
import com.jinmdz.model.chat.ChatMsg;

/**
 * @author lvyq
 * @version 1.0
 * @description: TODO
 * @date 2021/11/12 13:18
 */
public interface ChatService {
    JSONObject msgHistory(ChatMsg data);

    JSONObject getUnReadNum(ChatMsg data);

    JSONObject getWebUrl();
}

ChatServiceImpl

import com.alibaba.fastjson.JSONObject;
import com.jinmdz.model.chat.ChatMsg;
import com.jinmdz.service.ChatService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author lvyq
 * @version 1.0
 * @description: TODO
 * @date 2021/11/12 13:19
 */
@Service
    public class ChatServiceImpl implements ChatService {

    @Resource
    private MongoTemplate mongoTemplate;

    @Value("${websocket.url}")
    public  String WebUrl;

    /**
     * @description: 消息记录
     * @author: lvyq
     * @date: 2021/9/26 15:20
     * @version 1.0
     */

    @Override
    public JSONObject msgHistory(ChatMsg data) {
        JSONObject mav = new JSONObject();
        int page = data.getCurrentPage();
        if (page<0){
            page=1;
        }
        page=page-1;
        int size = data.getPageSize();
        if (size<0){
            size=20;
        }
        if (!StringUtils.isEmpty(data.getSendId()) && !StringUtils.isEmpty(data.getReceiveId())){
            String msgKeyOne= data.getSendId()+"_"+data.getReceiveId();
            String msgKeyTwo = data.getReceiveId()+"_"+data.getSendId();
            Criteria orCri1 = new Criteria();
            Criteria orCri2 = new Criteria();
            orCri1.andOperator(Criteria.where("msgKey").is(msgKeyOne));
            orCri2.andOperator(Criteria.where("msgKey").is(msgKeyTwo));
            Query query = new Query().addCriteria(new Criteria().orOperator(orCri1,orCri2));
            long total = mongoTemplate.count(query,ChatMsg.class);
            Pageable pageable = PageRequest.of(page,size, Sort.by(Sort.Direction.DESC,"createTime"));
            query.with(pageable);
            List<ChatMsg> list = mongoTemplate.find(query,ChatMsg.class);
            HashMap<String, Object> map = new HashMap<>();
            if (list != null) {
                for (ChatMsg chatMsg:list){
                    chatMsg.setName(chatMsg.getChatMsg());
                    if (chatMsg.getSendId().equals(data.getSendId())){
                        chatMsg.setFlag(1);
                    }else {
                        chatMsg.setFlag(0);
                    }
                }
                Map<String, Object> pageMap = new HashMap<>();
                map.put("list", list);
                pageMap.put("total", total);
                pageMap.put("pageSize", data.getPageSize());
                pageMap.put("currentPage", data.getCurrentPage());
                map.put("pager", pageMap);
            }
            Query queryMsg = new Query().addCriteria(Criteria.where("readState").is("0").and("sendId").is(data.getReceiveId()).and("receiveId").is(data.getSendId()));
            Update update = new Update().set("readState","1");
            mongoTemplate.updateFirst(queryMsg,update,ChatMsg.class);
            mav.put("state",true);
            mav.put("data",map);
            return mav;
        }else {
            mav.put("state",false);
            mav.put("msg","非法参数");
            return mav;
        }

    }

    /**
     * @description: 未读消息条数
     * @author: lvyq
     * @date: 2021/11/16 14:20
     * @version 1.0
     */
    
    @Override
    public JSONObject getUnReadNum(ChatMsg data) {
        JSONObject mav = new JSONObject();
        Query query = new Query();
        query.addCriteria(Criteria.where("readState").is("0").and("sendId").is(data.getSendId()).and("receiveId").is(data.getReceiveId()));
        long count = mongoTemplate.count(query,ChatMsg.class);
        mav.put("state",true);
        mav.put("unReadNum",count);
        return mav;
    }


    

    /**
     * @description: 获取websocket地址
     * @author: lvyq
     * @date: 2021/11/16 14:47
     * @version 1.0
     */
    @Override
    public JSONObject getWebUrl() {
        JSONObject mav = new JSONObject();
        mav.put("state",true);
        mav.put("webUrl",WebUrl);
        return mav;
    }

}

如有问题可在下方留言💬
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不要喷香水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值