美文网首页
开发一个在线聊天

开发一个在线聊天

作者: 邹小邹大厨 | 来源:发表于2022-08-16 16:38 被阅读0次

    在线聊天技术选型

    在线聊天因为涉及到互相通信,所以采用socket.io

    前端框架 vue2

    打包工具 vite

    在线gitee地址: https://gitee.com/service-chat/service-chat

    整体架构

    初始化之后的效果如下:

    init 初始化

    init 主要是从url参数中获取用户的id,然后调用signalrService

        // 初始化
        init() {
          this.sender.id = parseInt(this.$route.query.sendId);
          if (!(this.sender.id > 0)) {
            alert("请添加sendId参数");
            return false;
          }
          // 当前产品
          let product = this.$store.state.productList.filter(
            (x) => x.Id === this.$route.query.productId
          );
          if (product.length > 0) {
            // 卡片信息内容
            this.browseCard.Id = product[0].Id;
            this.browseCard.Name = product[0].Name;
            this.browseCard.ShortDescription = product[0].ShortDescription;
            this.browseCard.DefaultPictureUrl = product[0].DefaultPictureUrl;
            this.browseCard.Amount = "编码:" + product[0].ProductCode;
            this.browseCard.Type = 1;
          }
          // 当前用户
          let userInfo = this.$store.state.userList.filter(
            (x) => x.id == this.sender.id
          )[0];
          // 快速回复
          this.fastReplay = this.$store.state.fastReply;
          if (userInfo) {
            this.sender.name = userInfo.name;
            // 修改昵称时的临时记录昵称
            this.temporaryUserName = userInfo.name;
            this.sender.isService = userInfo.isService;
            this.sender.receptNum = userInfo.receptNum;
            // 修改接待用户数量时的临时记录接待用户数量
            this.temporaryReceptNumber = userInfo.receptNum;
          } else {
            alert("请保证sendId参数在userList.json文件中存在");
            return false;
          }
          // 发送欢迎语
          let welCome = this.$store.state.robotReply.filter(
            (x) => x.Answer.indexOf("欢迎语") !== -1
          );
          if (welCome.length > 0) {
            this.signalrService(welCome[0], 1, 4, false);
          }
        },
    

    signalrService

    当初次初始化的时候,只是把当前的内容发送到当前会话内容里边去。

    // 1.信息组装
    // 发送者身份:0 机器人,1 客服员,2.会员
    // 信息类型 :0 文本,1 图片,2 表情,3 商品卡片/订单卡片,4 机器人回复
        signalrService(
          content,
          identity,
          type,
          isSendOther = true,
          isRobot = false
        ) {
          // 发送信息
          if (this.sendState) {
            let createDate = this.nowTime();
            let noCode = +new Date();
            this.infoTemplate = {
              SendId: this.sender.id,
              ReviceId: isRobot ? 0 : this.revicer.id,
              Content: content,
              Identity: identity,
              Type: type,
              State: isRobot || !this.sender.onlineState ? 1 : 0,
              // 发送时间戳
              NoCode: noCode,
              OutTradeNo: this.revicer.outTradeNo,
              CreateDateUtc: createDate,
              Title: null,
              Description: null,
              Label: null,
              Thumbnail: null,
              NoSend: true,
            };
            // 发送到当前会员内容里边中
            this.toSendInfo(this.infoTemplate);
            if (isSendOther) {
              this.sendMsg(this.infoTemplate);
            }
            this.sendState = isRobot || !this.sender.onlineState ? true : false;
            this.sendInfo = type === 2 ? this.sendInfo : "";
            this.toBottom(100);
          } else {
            this.showMsg("发送太快啦,请稍后再试");
          }
        }
    

    和机器人对话

    如果客服是机器人的话,用户依然可以发送一些信息给机器人,比如发送一些信息,效果如下:

    当然也可以点击机器人发送过来的信息,比如查看如何操作退款,如何操作提货等

    发送信息给机器人

    可以和机器人聊天,可以把一些用户常见的问题,形成标准答案,当用户输入的问题的时候,如果用户输入的问题在问题库里边,可以直接按照标准问题答案进行回复。

    发送消息给机器人是使用的sendToRobot

     // 机器人聊天
    sendToRobot() {
      console.log(1223);
      if (this.sendInfo != "") {
        let createDate = this.nowTime();
        let noCode = +new Date();
        let content = this.sendInfo;
        this.sendInfo = "";
        // 封装消息
        this.infoTemplate = {
          SendId: this.sender.id,
          ReviceId: 0,
          Content: content,
          Identity: 2,
          Type: 0,
          State: 0,
          NoCode: noCode,
          OutTradeNo: null,
          CreateDateUtc: createDate,
          Title: null,
          Description: null,
          Label: null,
          Thumbnail: null,
          NoSend: true,
        };
        // 把消息加入到消息会话内容里边
        this.toSendInfo(this.infoTemplate);
        // 把信息拉到最低下,因为消息需要展示最新的
        this.toBottom(100);
        // 触发socket的sendToRobot事件
        this.socket.emit("sendToRobot", this.infoTemplate);
        // 设定一个时间,如果超过了固定时间,就设置为发送失败
        this.sendFailed(this.infoTemplate);
      } else {
        return null;
      }
    }
    

    在后端接收sendToRobot事件,然后看看是否有发送过来问题的固定答案,然后触发changOrShowMsg

    //发送信息给机器人
    socket.on("sendToRobot", (data) => {
      let welCome = robotReply.filter(
        (x) => x.Answer.indexOf(data.Content) !== -1
      );
      socket.emit("reviceFromRobot", {
        content:
          welCome.length > 0
            ? welCome[0]
            : "非常对不起哦,不知道怎么回答这个问题呢,我会努力学习的。",
        flag: welCome.length > 0 ? true : false,
      });
      socket.emit("changOrShowMsg", data);
    });
    

    当前端接收到changOrShowMsg后,把消息设置为发送成功

    // 修改信息状态
    this.socket.on("changOrShowMsg", (data) => {
      this.sendState = true;
      // 清除sendFailed设置的定时器,然后设置成功
      clearTimeout(this.msgTimer);
      this.conversition.forEach((x) => {
        if (x.NoCode !== null && x.NoCode === data.NoCode) {
          x.State = 1;
        }
      });
    });
    

    人工聊天

    如果觉得客服机器人不能满足需求的时候,可以通过点击转人工转人工客服,和京东淘宝都类似,因为很多情况下,机器人都不能满足用户的需求,所以需要转人工

    客服不在线

    调用函数是callPeople

    // 呼叫客服
    callPeople() {
      // 显示loading
      this.loading();
      // 呼叫客服
      this.joinChat();
    },
    

    呼叫客服,其实就是看看有没有客服在线

    //加入会话
    joinChat() {
      // 呼叫客服
      this.socket.emit("joinChat", {
        SendId: this.sender.id,
        ReviceId: this.revicer.id,
        SendName: this.sender.name,
        ReviceName: this.revicer.name,
        IsService: this.sender.isService,
        NoCode: this.noCode,
      });
    },
    

    在后端监听joinChat事件,逻辑比较清晰,就是监听到有用户想加入进来的时候,判断当前的是否有客服在线,如果有客服在线,则看下是否有空闲时间的客服,如果每个客服都很忙,达到了最大服务用户数量,则显示客服较忙,稍微再等会,如果有空闲的客服,则把客服分配服务于当前用户。

    // 加入聊天
    socket.on("joinChat", (data) => {
      let serviceList = null;
      let index = 0;
      // 如果发送消息的不是客服
      if (!data.IsService) {
        // 当前登录的客服列表
        serviceList = users.filter((x) => x.IsService === true);
        // 当前登录的客服列表的人数
        let serviceCount = serviceList.length;
        for (let i = serviceCount - 1; i >= 0; i--) {
          let item = serviceList[i];
          // 当前登录的用户列表
          let number = users.filter((x) => x.ReviceId === item.SendId).length;
          // 当前客服可以接待的最大用户数量
          let num = userList.filter((x) => x.id === item.SendId)[0].receptNum;
          // 如果当前登录的用户数量大于当前客服可以接待的数量,把该客服删除
          if (number >= num) {
            serviceList.splice(i, 1);
          }
        }
        // 如果当前登录的客服数量大于0并且每个客服已经达到的最大的服务用户数量
        if (serviceCount > 0 && serviceList.length <= 0) {
          socket.emit("joinError", {
            msg: "当前咨询人数较多,请稍后再试",
          });
          return;
          // 还有剩余客服
        } else if (serviceList.length > 0) {
          // 随机分配客服
          index = randomNum(0, serviceList.length - 1);
          socket.emit("joinTip", {
            ReviceName: serviceList[index].SendName,
            ReviceId: serviceList[index].SendId,
            ReviceOutTradeNo: serviceList[index].OutTradeNo,
          });
          // 让会员加入房间
          socket.join(serviceList[index].OutTradeNo);
          // 如果没有客服在线,则返回暂无客服在线
        } else {
          socket.emit("joinError", {
            msg: "暂无客服在线",
          });
          return;
        }
      } else {
        // 如果发送消息的是客服,则加入到聊天室里边
        socket.join(socket.id);
      }
      // 若该用户已登录,将旧设备登录的用户强制下线,多个用户多端登录
      let oldUser = users.filter((x) => x.SendId === data.SendId);
      if (oldUser.length > 0) {
        socket.to(oldUser[0].OutTradeNo).emit("squeezeOut", {
          noCode: oldUser[0].NoCode,
        });
      }
      // 存在用户信息时将旧记录删除并且重新记录
      users = users.filter((x) => x.SendId !== data.SendId);
      let user = {
        SendId: data.SendId,
        SendName: data.SendName,
        ReviceId: serviceList ? serviceList[index].SendId : data.ReviceId,
        ReviceName: serviceList ? serviceList[index].SendName : data.ReviceName,
        NoCode: data.NoCode,
        OutTradeNo: socket.id,
        Room: data.IsService ? socket.io : serviceList[index].OutTradeNo,
        IsService: data.IsService,
        IsSelect: false,
        SessionContent: data.SendName + "加入会话",
        UnRead: 0,
        CloseSession: false,
      };
      // 用户重新加入
      users.push(user);
    
      // 把登录成功的sendId记录下来
      socket.SendId = data.SendId;
      io.emit("joinSuccess", {
        user,
        users,
      });
    });
    

    前面没有上线客服,所以当用户想转人工的时候,只能显示暂无客服,现在看下客服端是什么样的。

    效果如下:


    客服可以设置上线或者离线,当客服上线之后,这个时候,当用户选择客服聊天后,就可以选择客服了。

    调用

    // 修改在线状态
    changeOnLine() {
      if (!this.sender.onlineState) {
        this.loading();
        // 客服上线
        this.socket.emit("joinChat", {
          SendId: this.sender.id,
          SendName: this.sender.name,
          ReviceId: -1,
          ReviceName: this.revicer.name,
          IsService: true,
          NoCode: this.noCode,
        });
      } else {
        // 离线
        this.loading();
        this.isSelectSession = false;
        this.socket.emit("offLine", {
          SendId: this.sender.id,
          NoCode: this.noCode,
        });
      }
    },
    
    

    后端如果接收到客服上线,就把客服加入到socket,也就是joinChat

    // 如果发送消息的是客服,则加入到聊天室里边
    socket.join(socket.id);
    

    如果客服已经在线了,就可以转人工和客服聊天了,

     // 随机分配客服
    index = randomNum(0, serviceList.length - 1);
    socket.emit("joinTip", {
      ReviceName: serviceList[index].SendName,
      ReviceId: serviceList[index].SendId,
      ReviceOutTradeNo: serviceList[index].OutTradeNo,
    });
    // 让会员加入房间
    socket.join(serviceList[index].OutTradeNo);
    

    可以看到后端接收到信息后,触发joinTip,然后用户就可以和客服聊天了。

    发送信息,通过后端通过sendMsg来处理

    // 发送消息
    socket.on("sendMsg", (data) => {
      // 设置用户未读
      users.map((x) => {
        if (x.SendId === data.SendId) {
          x.SessionContent = data.Content;
          x.UnRead = 1;
          return x;
        }
      });
      //
      let sender = users.filter((x) => x.SendId === data.SendId);
      let revicer = users.filter((x) => x.SendId === data.ReviceId);
      if (sender.length < 0) {
        socket.emit("offLineTip", {
          msg: "您已掉线,请重新连接",
        });
        return;
      }
      if (revicer.length < 0) {
        socket.emit("offLineTip", {
          msg: "对方已离线",
        });
        return;
      }
      data.State = 1;
      // 向socket触发reviceMsg
      socket.to(data.OutTradeNo).emit("reviceMsg", data);
      socket.emit("changOrShowMsg", data);
    });
    

    可以看到,是通过socket.to(data.OutTradeNo).emit("reviceMsg", data); 来触发

    // 接收信息
    this.socket.on("reviceMsg", (data) => {
      if (this.sender.isService && data.ReviceId == this.sender.id) {
        this.playMusic();
        this.currentSessionPeople.forEach((x) => {
          if (x.SendId === data.SendId) {
            if (!x.IsSelect) x.UnRead++;
            switch (data.Type) {
              case 0:
                x.SessionContent = data.Content;
                break;
              case 1:
                x.SessionContent = "图片";
                break;
              case 2:
                x.SessionContent = "表情";
                break;
              case 3:
                x.SessionContent = "卡片";
                break;
            }
          }
        });
      }
      if (this.sender.onlineState) this.toSendInfo(data);
    });
    

    发送图片

    不管是用户或者是客服发送图片都是调用sendMsg

    //发送图片
    sendImage(e) {
      const fileObj = e.target.files[0];
      let identity = this.sender.isService ? 1 : 2;
      if (fileObj != null) {
        // 判断是否是图片
        if (!/image\/\w+/.test(fileObj.type)) {
          return alert("请选择图片文件!", { icon: 5, time: 1000 });
        }
        var fd = new FormData();
        fd.append("file", fileObj);
        // 判断图片大小
        if (fileObj.size > 1024 * 1024 * 2 && fileObj.size < 1024 * 1024 * 10) {
          let reader = new FileReader();
          reader.readAsDataURL(fileObj);
          reader.onload = (e) => {
            let image = new Image(); //新建一个img标签(还没嵌入DOM节点)
            image.src = e.target.result;
            image.onload = () => {
              let canvas = document.createElement("canvas"),
                context = canvas.getContext("2d"),
                imageWidth = image.width / 2, //压缩后图片的大小
                imageHeight = image.height / 2,
                data = "";
              canvas.width = imageWidth;
              canvas.height = imageHeight;
              context.drawImage(image, 0, 0, imageWidth, imageHeight);
              data = canvas.toDataURL("image/jpeg");
              let newFile = this.dataURLtoFile(data); //压缩完成
              fd = new FormData();
              fd.append("file", newFile);
              // 显示出来
              this.signalrService(data, identity, 1);
              this.$refs.referenceUpload.value = null;
            };
          };
        } else if (fileObj.size > 1024 * 1024 * 10) {
          return alert("上传图片不能超过10M!", { icon: 5, time: 1000 });
        } else {
          let reader = new FileReader();
          reader.readAsDataURL(fileObj);
          reader.onload = (e) => {
            this.signalrService(e.target.result, identity, 1);
            this.$refs.referenceUpload.value = null;
          };
        }
      }
    },
    

    后面的处理就和发送文字类似了

    发送表情

    发送表情是直接把图片作为发送内容进行发送的,使用如下代码:

    <template v-for="(item, index) in expressions">
      <li>
        <img
          class="customerSendExpression"
          v-bind:src="item.image"
          v-bind:title="item.title"
          @click="toSend(item.image, 2, 2)"
        />
      </li>
    </template>
    

    本文由mdnice多平台发布

    相关文章

      网友评论

          本文标题:开发一个在线聊天

          本文链接:https://www.haomeiwen.com/subject/atqdgrtx.html