YYGH-13-客服中心

客服中心

最近又想了一个功能客服中心,可以实现管理端和用户端之间进行交互

思路

1.客户端和管理端前端分别添加一个聊天入口

2.建立一个chat 模块整合webSocket实现聊天室的后端支持

3.持久化层选用的mongodb,存放建立一个表chat(id,发送人id,接收人id,发送内容,发送时间)存放聊天记录,同时会利用mysql表同步的功能,新建一个库yygh_chat用于同步yygh_user中的user_info,因为聊天记录需要知道是谁发送的

客户端

新建一个chat模块,负责管理我们的聊天记录和聊天室

我们的客服系统设置在帮助中心

image-20220729213524500

这里我使用的是开源组件https://github.com/Coffcer/vue-chat

image-20220801150501703

在一名前端大佬的支援之下我把这个聊天室做到了这样,现在有1个bug发送一次会收到两次消息

image-20220801151007954

是因为这样我现在把这个删除就ok了,还要给他添加一个头像

前端页面

<template>
    <div class="page-container">
      <div class="chat-box">
      <header>聊天室 (在线:{{ count }})</header>
      <div class="msg-box" ref="msg-box">
        <div v-for="(i,index) in list"
             :key="index"
             class="msg"
             :style="i.token === token?'flex-direction:row-reverse':''"
        >
          <div class="user-head">
            <img :src="'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80'" height="30" width="30" :title="i.username">
          </div>
          <div class="user-msg">
          <span :style="i.token === token?' float: right;':''"
                :class="i.token === token?' right':'left'">{{ i.content }}</span>
          </div>
        </div>
      </div>
      <div class="input-box">
        <input type="text" ref="sendMsg" v-model="contentText" @keyup.enter="sendText()"/>
        <div class="btn" :class="{['btn-active']:contentText}" @click="sendText()">发送</div>
      </div>
    </div>
    </div>
</template>

<script>
import cookie from "js-cookie";

export default {
  data() {
    return {
      ws: null,
      count: 0,
      token: null, // 当前用户ID
      username: null, // 当前用户昵称
      avatar: null, // 当前用户头像
      list: [], // 聊天记录的数组
      contentText: "" // input输入的值
    };
  },
  created() {
    this.showInfo()
  },
  mounted() {
    this.initWebSocket();
  },
  destroyed() {
    // 离开页面时关闭websocket连接
    this.ws.onclose(undefined);
  },
  methods: {
    // 发送聊天信息
    sendText() {
      let _this = this;
      _this.$refs["sendMsg"].focus();
      if (!_this.contentText) {
        return;
      }
      let params = {
        token: _this.token,
        username: _this.username,
        avatar: _this.avatar,
        msg: _this.contentText,
        count: _this.count
      };
      _this.ws.send(JSON.stringify(params)); //调用WebSocket send()发送信息的方法
      _this.contentText = "";
      setTimeout(() => {
        _this.scrollBottm();
      }, 500);
    },
    // 进入页面创建websocket连接
    initWebSocket() {
      let _this = this;
      // 判断页面有没有存在websocket连接
      if (window.WebSocket) {
        var serverHot = window.location.hostname;
        let sip = '1'
        // 填写本地IP地址 此处的 :9101端口号 要与后端配置的一致!
        let token = cookie.get('token');
        var url = 'ws://' + serverHot + ':8209' + '/api/chat/' + sip + '/' + token; // `ws://127.0.0.1:8209/api/chat/1`
        console.log(url)
        let ws = new WebSocket(url);
        _this.ws = ws;
        ws.onopen = function (e) {
          console.log("服务器连接成功: " + url);
        };
        ws.onclose = function (e) {
          console.log("服务器连接关闭: " + url);
        };
        ws.onerror = function () {
          console.log("服务器连接出错: " + url);
        };
        ws.onmessage = function (e) {
          //接收服务器返回的数据
          let resData = JSON.parse(e.data)
          _this.count = resData.count;
          _this.list = [
            ..._this.list,
            {token: resData.token, username: resData.username, avatar: resData.avatar, content: resData.msg}
          ];
        };
      }
    },
    // 滚动条到底部
    scrollBottm() {
      let el = this.$refs["msg-box"];
      el.scrollTop = el.scrollHeight;
    },
    showInfo() {
      this.token = cookie.get('token')
      if (this.token) {
        this.username = cookie.get('name')
        console.log(this.username)
        console.log(this.token)
      }
    },
  }
};
</script>

<style lang="scss" scoped>
.page-container{
  height: 700px;
}
.chat-box {
  margin: 0 auto;
  background: #fafafa;
  position: absolute;
  height: 95%;
  width: 100%;

  header {
    width: 100%;
    height: 3rem;
    background: #409eff;
    display: flex;
    justify-content: center;
    align-items: center;
    font-weight: bold;
    color: white;
    font-size: 1rem;
  }

  .msg-box {
    position: absolute;
    height: calc(100% - 6.5rem);
    width: 100%;
    margin-top: 3rem;
    overflow-y: scroll;

    .msg {
      width: 95%;
      min-height: 2.5rem;
      margin: 1rem 0.5rem;
      position: relative;
      display: flex;
      justify-content: flex-start !important;

      .user-head {
        min-width: 2.5rem;
        width: 20%;
        width: 2.5rem;
        height: 2.5rem;
        border-radius: 50%;
        background: #f1f1f1;
        display: flex;
        justify-content: center;
        align-items: center;

        .head {
          width: 1.2rem;
          height: 1.2rem;
        }

        // position: absolute;
      }

      .user-msg {
        width: 80%;
        // position: absolute;
        word-break: break-all;
        position: relative;
        z-index: 5;

        span {
          display: inline-block;
          padding: 0.5rem 0.7rem;
          border-radius: 0.5rem;
          margin-top: 0.2rem;
          font-size: 0.88rem;
        }

        .left {
          background: white;
          animation: toLeft 0.5s ease both 1;
        }

        .right {
          background: #53a8ff;
          color: white;
          animation: toright 0.5s ease both 1;
        }

        @keyframes toLeft {
          0% {
            opacity: 0;
            transform: translateX(-10px);
          }
          100% {
            opacity: 1;
            transform: translateX(0px);
          }
        }
        @keyframes toright {
          0% {
            opacity: 0;
            transform: translateX(10px);
          }
          100% {
            opacity: 1;
            transform: translateX(0px);
          }
        }
      }
    }
  }

  .input-box {
    padding: 0 0.5rem;
    position: absolute;
    bottom: 0;
    width: 100%;
    height: 3.5rem;
    background: #fafafa;
    box-shadow: 0 0 5px #ccc;
    display: flex;
    justify-content: space-between;
    align-items: center;

    input {
      height: 2.3rem;
      display: inline-block;
      width: 100%;
      padding: 0.5rem;
      border: none;
      border-radius: 0.2rem;
      font-size: 0.88rem;
    }

    .btn {
      height: 2.3rem;
      min-width: 4rem;
      background: #e0e0e0;
      padding: 0.5rem;
      font-size: 0.88rem;
      color: white;
      text-align: center;
      border-radius: 0.2rem;
      margin-left: 0.5rem;
      transition: 0.5s;
    }

    .btn-active {
      background: #409eff;
    }
  }
}
</style>

webSocket

@Slf4j
@Service
@ServerEndpoint(value = "/api/chat/{sid}/{token}")
public class WebSocketServerController {

    private static ApplicationContext applicationContext;

    public static void setApplicationContext(ApplicationContext applicationContext) {
        WebSocketServerController.applicationContext = applicationContext;
    }

    /**
     * 房间号 -> 组成员信息
     */
    private static ConcurrentHashMap<String, List<Session>> groupMemberInfoMap = new ConcurrentHashMap<>();
    /**
     * 房间号 -> 在线人数
     */
    private static ConcurrentHashMap<String, Set<String>> onlineUserMap = new ConcurrentHashMap<>();

    /**
     * 收到消息调用的方法,群成员发送消息
     *
     * @param sid:房间号
     * @param token:用户token
     * @param message:发送消息
     */
    @OnMessage
    public void onMessage(@PathParam("sid") String sid, @PathParam("token") String token, String message) {
        // json字符串转对象
        MsgVO msg = JSONObject.parseObject(message, MsgVO.class);

        //新建一个聊天记录
        MsgEntity msgEntity = new MsgEntity();
        Long userId;
        if (token.equals("admin")) {
            userId = 0L;
        } else {
            userId = JwtHelper.getUserId(token);
        }
        msgEntity.setUserName(msg.getUsername());
        msgEntity.setMsg(msg.getMsg());
        msgEntity.setUserId(userId);
        WebSocketService webSocketService = applicationContext.getBean(WebSocketService.class);
        webSocketService.saveMsg(msgEntity);
        List<Session> sessionList = groupMemberInfoMap.get(sid);
        Set<String> onlineUserList = onlineUserMap.get(sid);
        // 先一个群组内的成员发送消息
        sessionList.forEach(item -> {
            try {
                msg.setCount(onlineUserList.size());
                // json对象转字符串
                String text = JSONObject.toJSONString(msg);
                item.getBasicRemote().sendText(text);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 建立连接调用的方法,群成员加入
     *
     * @param session
     * @param sid
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid, @PathParam("token") String token) {
        List<Session> sessionList = groupMemberInfoMap.computeIfAbsent(sid, k -> new ArrayList<>());
        Set<String> onlineUserList = onlineUserMap.computeIfAbsent(sid, k -> new HashSet<>());
        onlineUserList.add(token);
        sessionList.add(session);
        // 发送上线通知
        sendInfo(sid, token, onlineUserList.size(), "上线了~");
    }


    public void sendInfo(String sid, String token, Integer onlineSum, String info) {
        log.info(token);
        if (Objects.equals(token, "admin")){
            MsgVO msg = new MsgVO();
            msg.setToken(token);
            msg.setUsername("客服");
            msg.setCount(onlineSum);
            msg.setMsg("客服" + info);
            // json对象转字符串
            String text = JSONObject.toJSONString(msg);
            onMessage(sid, token, text);
            return;
        }
        Long userId = JwtHelper.getUserId(token);
        // 获取该连接用户信息
        WebSocketService webSocketService = applicationContext.getBean(WebSocketService.class);
        UserInfo userInfo = webSocketService.getUserInfo(userId);
        // 发送通知
        MsgVO msg = new MsgVO();
        msg.setToken(token);
        msg.setUsername(userInfo.getNickName());
        msg.setCount(onlineSum);
        msg.setMsg(userInfo.getNickName() + info);
        // json对象转字符串
        String text = JSONObject.toJSONString(msg);
        onMessage(sid, token, text);
    }

    /**
     * 关闭连接调用的方法,群成员退出
     *
     * @param session
     * @param sid
     */
    @OnClose
    public void onClose(Session session, @PathParam("sid") String sid, @PathParam("token") String token) {
        List<Session> sessionList = groupMemberInfoMap.get(sid);
        sessionList.remove(session);
        Set<String> onlineUserList = onlineUserMap.get(sid);
        onlineUserList.remove(token);
        // 发送离线通知
        sendInfo(sid, token, onlineUserList.size(), "下线了~");
    }

    /**
     * 传输消息错误调用的方法
     *
     * @param error
     */
    @OnError
    public void OnError(Throwable error) {
        log.info("Connection error");
    }
}
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
@Component
public class ApplicationContextProvider implements ApplicationContextAware {

    private static ApplicationContext applicationContextSpring;
    @Override
    public synchronized void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        applicationContextSpring = applicationContext;
    }
    public static <T> T getBean(Class<T> clazz) {

        return applicationContextSpring.getBean(clazz);
    }

}

image-20220801151432191

image-20220801153230930

管理端

在websocket中添加了一个这样的判断

log.info(token);
if (Objects.equals(token, "admin")){
    MsgVO msg = new MsgVO();
    msg.setToken(token);
    msg.setUsername("客服");
    msg.setCount(onlineSum);
    msg.setMsg("客服" + info);
    // json对象转字符串
    String text = JSONObject.toJSONString(msg);
    onMessage(sid, token, text);
    return;
}

image-20220801153218049

<template>
  <div class="page-container">
    <div class="chat-box">
      <header>聊天室 (在线:{{ count }})</header>
      <div class="msg-box" ref="msg-box">
        <div v-for="(i,index) in list"
             :key="index"
             class="msg"
             :style="i.token === token?'flex-direction:row-reverse':''"
        >
          <div class="user-head">
            <img :src="'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80'"
                 height="30" width="30" :title="i.username">
          </div>
          <div class="user-msg">
          <span :style="i.token === token?' float: right;':''"
                :class="i.token === token?' right':'left'">{{ i.content }}</span>
          </div>
        </div>
      </div>
      <div class="input-box">
        <input type="text" ref="sendMsg" v-model="contentText" @keyup.enter="sendText()"/>
        <div class="btn" :class="{['btn-active']:contentText}" @click="sendText()">发送</div>
      </div>
    </div>
  </div>
</template>

<script>

export default {
  data() {
    return {
      ws: null,
      count: 0,
      token: 'admin', // 当前用户ID
      username: '客服', // 当前用户昵称
      avatar: null, // 当前用户头像
      list: [], // 聊天记录的数组
      contentText: '' // input输入的值
    }
  },
  mounted() {
    this.initWebSocket()
  },
  destroyed() {
    // 离开页面时关闭websocket连接
    this.ws.onclose(undefined)
  },
  methods: {
    // 发送聊天信息
    sendText() {
      let _this = this
      _this.$refs['sendMsg'].focus()
      if (!_this.contentText) {
        return
      }
      let params = {
        token: _this.token,
        username: _this.username,
        avatar: _this.avatar,
        msg: _this.contentText,
        count: _this.count
      }
      _this.ws.send(JSON.stringify(params))
      _this.contentText = ''
      setTimeout(() => {
        _this.scrollBottm()
      }, 500)
    },
    // 进入页面创建websocket连接
    initWebSocket() {
      let _this = this
      // 判断页面有没有存在websocket连接
      if (window.WebSocket) {
        var serverHot = window.location.hostname
        let sip = '1'
        // 填写本地IP地址 此处的 :9101端口号 要与后端配置的一致!
        var url = 'ws://' + serverHot + ':8209' + '/api/chat/' + sip + '/admin';
        let ws = new WebSocket(url)
        _this.ws = ws
        ws.onopen = function(e) {
          console.log('服务器连接成功: ' + url)
        }
        ws.onclose = function(e) {
          console.log('服务器连接关闭: ' + url)
        }
        ws.onerror = function() {
          console.log('服务器连接出错: ' + url)
        }
        ws.onmessage = function(e) {
          let resData = JSON.parse(e.data)
          _this.count = resData.count
          _this.list = [
            ..._this.list,
            { token: resData.token, username: resData.username, avatar: resData.avatar, content: resData.msg }
          ]
        }
      }
    },
    // 滚动条到底部
    scrollBottm() {
      let el = this.$refs['msg-box']
      el.scrollTop = el.scrollHeight
    }
  }
}
</script>

<style lang="scss" scoped>
.page-container {
  height: 700px;
}

.chat-box {
  margin: 0 auto;
  background: #fafafa;
  position: absolute;
  height: 95%;
  width: 100%;

  header {
    width: 100%;
    height: 3rem;
    background: #409eff;
    display: flex;
    justify-content: center;
    align-items: center;
    font-weight: bold;
    color: white;
    font-size: 1rem;
  }

  .msg-box {
    position: absolute;
    height: calc(100% - 6.5rem);
    width: 100%;
    margin-top: 3rem;
    overflow-y: scroll;

    .msg {
      width: 95%;
      min-height: 2.5rem;
      margin: 1rem 0.5rem;
      position: relative;
      display: flex;
      justify-content: flex-start !important;

      .user-head {
        min-width: 2.5rem;
        width: 20%;
        width: 2.5rem;
        height: 2.5rem;
        border-radius: 50%;
        background: #f1f1f1;
        display: flex;
        justify-content: center;
        align-items: center;

        .head {
          width: 1.2rem;
          height: 1.2rem;
        }

        // position: absolute;
      }

      .user-msg {
        width: 80%;
        // position: absolute;
        word-break: break-all;
        position: relative;
        z-index: 5;

        span {
          display: inline-block;
          padding: 0.5rem 0.7rem;
          border-radius: 0.5rem;
          margin-top: 0.2rem;
          font-size: 0.88rem;
        }

        .left {
          background: white;
          animation: toLeft 0.5s ease both 1;
        }

        .right {
          background: #53a8ff;
          color: white;
          animation: toright 0.5s ease both 1;
        }

        @keyframes toLeft {
          0% {
            opacity: 0;
            transform: translateX(-10px);
          }
          100% {
            opacity: 1;
            transform: translateX(0px);
          }
        }
        @keyframes toright {
          0% {
            opacity: 0;
            transform: translateX(10px);
          }
          100% {
            opacity: 1;
            transform: translateX(0px);
          }
        }
      }
    }
  }

  .input-box {
    padding: 0 0.5rem;
    position: absolute;
    bottom: 0;
    width: 100%;
    height: 3.5rem;
    background: #fafafa;
    box-shadow: 0 0 5px #ccc;
    display: flex;
    justify-content: space-between;
    align-items: center;

    input {
      height: 2.3rem;
      display: inline-block;
      width: 100%;
      padding: 0.5rem;
      border: none;
      border-radius: 0.2rem;
      font-size: 0.88rem;
    }

    .btn {
      height: 2.3rem;
      min-width: 4rem;
      background: #e0e0e0;
      padding: 0.5rem;
      font-size: 0.88rem;
      color: white;
      text-align: center;
      border-radius: 0.2rem;
      margin-left: 0.5rem;
      transition: 0.5s;
    }

    .btn-active {
      background: #409eff;
    }
  }
}
</style>

聊天记录

image-20220802130414587

聊天记录和之前规划的一样采用mongodb,现在有一个问题

@Data
@Document("Msg")
public class MsgEntity extends BaseMongoEntity {

    @ApiModelProperty(value = "用户ID")
    private Long userId;

    @ApiModelProperty(value = "用户名")
    private String userName;
 
    @ApiModelProperty(value = "消息")
    private String msg;

}

这是我的里面的用户名需要调用user获取用户名但是如果没保存一次就需要去user调用一次未免也太过于消耗性能,于是这里我的构想是在保存的时候不给用户名,等到管理页面需要查看的时候再统一进行查询来实现一个延迟加载,后来我发现,我的前端可以获取userid,userName

@Service
public class WebSocketServiceImpl implements WebSocketService {

    @Autowired
    private PatientFeignClient patientFeignClient;

    @Autowired
    private MsgRepository msgRepository;

    @Override
    public UserInfo getUserInfo(Long id) {
        return patientFeignClient.getUserInfo(id);
    }

    @Override
    public void saveMsg(MsgEntity msgEntity) {
        msgEntity.setCreateTime(new Date());
        msgRepository.save(msgEntity);
    }

    @Override
    public Page<MsgEntity> selectPage(Integer page, Integer limit, String userName) {
        //创建Pageable对象
        Pageable pageable = PageRequest.of(page - 1, limit);
        //创建条件匹配器
        MsgEntity msgEntity = new MsgEntity();
        msgEntity.setUserName(userName);
        Example<MsgEntity> example = Example.of(msgEntity);
        return msgRepository.findAll(example, pageable);
    }
}
@Repository
public interface MsgRepository extends MongoRepository<MsgEntity,String> {
}

前端页面

<template>
  <div class="app-container">
    聊天记录
    <el-form :inline="true" class="demo-form-inline">
      <el-form-item>
        <el-input v-model="serchObj.userName" placeholder="用户名称"/>
      </el-form-item>
      <el-button type="primary" icon="el-icon-search" @click="getList()">查询</el-button>
    </el-form>

    <el-table :data="list" stripe style="width: 100%" @selection-change="handleSelectionChange">
      <el-table-column prop="userName" label="用户名称"/>
      <el-table-column prop="msg" label="聊天内容" width="1000"/>
      <el-table-column prop="createTime" label="聊天时间"/>
    </el-table>
    <el-pagination
      :current-page="page"
      :page-size="limit"
      :total="total"
      style="padding: 30px 0; text-align: center"
      layout="total, prev, pager, next, jumper"
      @current-change="getList"/>
  </div>
</template>

<script>
// 引入接口定义的js文件
import chatApi from '@/api/chat'

export default {
  data() {
    return {
      current: 1, // 当前页
      limit: 3, // 一个页显示的记录数
      serchObj: {}, // 条件封装对象
      list: [], // 每页数据集合
      total: 0,
      multipleSelection: [] // 批量选择中选择的记录列表
    }
  },

  methods: {

    handleSelectionChange(selection) {
      this.multipleSelection = selection
    },
    getList(page = 1) {
      this.current = page
      chatApi.chatList(this.current, this.limit, this.serchObj)
        .then((Response) => {
          this.list = Response.data.content
          this.total = Response.data.total
        }) //请求成功
        .catch((error) => {
          console.log(error)
        })
    }

  }
}
</script>
export default {

  chatList(page, limit, serchObj) {
    return request({
      url: `admin/chat/list/${page}/${limit}`,
      method: 'get',
      params: serchObj
    })
  }
}

项目的地址:https://github.com/xiaozhaotongzhide/YYGH

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值