当前位置:网站首页>YYGH-13-客服中心
YYGH-13-客服中心
2022-08-05 03:10:00 【小赵呢】
客服中心
最近又想了一个功能客服中心,可以实现管理端和用户端之间进行交互
思路
1.客户端和管理端前端分别添加一个聊天入口
2.建立一个chat 模块整合webSocket实现聊天室的后端支持
3.持久化层选用的mongodb,存放建立一个表chat(id,发送人id,接收人id,发送内容,发送时间)存放聊天记录,同时会利用mysql表同步的功能,新建一个库yygh_chat用于同步yygh_user中的user_info,因为聊天记录需要知道是谁发送的
客户端
新建一个chat模块,负责管理我们的聊天记录和聊天室
我们的客服系统设置在帮助中心
这里我使用的是开源组件https://github.com/Coffcer/vue-chat
在一名前端大佬的支援之下我把这个聊天室做到了这样,现在有1个bug发送一次会收到两次消息
是因为这样我现在把这个删除就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);
}
}
管理端
在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;
}
<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>
聊天记录
聊天记录和之前规划的一样采用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
边栏推荐
- undo问题
- 链表的简单描述及代码的简单实现
- On governance and innovation, the 2022 OpenAtom Global Open Source Summit OpenAnolis sub-forum came to a successful conclusion
- UOS系统下ksql应用缺少动态库”libtinfo.so.5“问题
- Use SuperMap iDesktopX data migration tool to migrate map documents and symbols
- 2022-08-04 第六小组 瞒春 学习笔记
- Thinking (88): Use protobuf custom options for multi-version management of data
- 21 Days Learning Challenge (2) Use of Graphical Device Trees
- AI + Small Nucleic Acid Drugs | Eleven Completes $22 Million Seed Round Financing
- [Filter tracking] based on matlab unscented Kalman filter inertial navigation + DVL combined navigation [including Matlab source code 2019]
猜你喜欢
Data storage practice based on left-order traversal
论治理与创新,2022 开放原子全球开源峰会 OpenAnolis 分论坛圆满落幕
腾讯云【Hiflow】新时代自动化工具
Why is the pca component not associated
Study Notes-----Left-biased Tree
IJCAI2022 | DictBert: Pre-trained Language Models with Contrastive Learning for Dictionary Description Knowledge Augmentation
On governance and innovation, the 2022 OpenAtom Global Open Source Summit OpenAnolis sub-forum came to a successful conclusion
数据增强Mixup原理与代码解读
Countdown to 2 days|Cloud native Meetup Guangzhou Station, waiting for you!
开发Hololens遇到The type or namespace name ‘HandMeshVertex‘ could not be found..
随机推荐
十五. 实战——mysql建库建表 字符集 和 排序规则
冒泡排序与快速排序
(十一)元类
The second council meeting of the Dragon Lizard Community was successfully held!Director general election, 4 special consultants joined
905. 区间选点
【七夕节】浪漫七夕,代码传情。将爱意变成绚烂的立体场景,给她(他)一个惊喜!(送代码)
北斗三号短报文终端露天矿山高边坡监测方案
Ant Sword Advanced Module Development
ASP.NET application--Hello World
2022高处安装、维护、拆除考试题模拟考试题库及在线模拟考试
How to solve the error cannot update secondary snapshot during a parallel operation when the PostgreSQL database uses navicat to open the table structure?
21 Days Learning Challenge (2) Use of Graphical Device Trees
Cloud Native (32) | Introduction to Platform Storage System in Kubernetes
Intersection of Boolean Operations in SuperMap iDesktop.Net - Repairing Complex Models with Topological Errors
Programmer's Tanabata Romantic Moment
你要的七夕文案,已为您整理好!
Flink 1.15.1 Cluster Construction (StandaloneSession)
Simple description of linked list and simple implementation of code
ASP.NET应用程序--Hello World
Snapback - same tree