当前位置:网站首页>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
边栏推荐
- The linear table lookup
- 【软件测试】自动化测试之unittest框架
- One hundred - day plan -- -- DAY2 brush
- Turn: Charles Handy: Who you are is more important than what you do
- From "useable" to "easy to use", domestic software is self-controllable and continues to advance
- 627. 变更性别
- 倒计时 2 天|云原生 Meetup 广州站,等你来!
- Review 51 MCU
- leetcode - symmetric binary tree
- 语法基础(变量、输入输出、表达式与顺序语句完成情况)
猜你喜欢

Countdown to 2 days|Cloud native Meetup Guangzhou Station, waiting for you!

The linear table lookup

Multithreading (2)

CPDA|How Operators Learn Data Analysis (SQL) from Negative Foundations

Why is the pca component not associated

通过模拟Vite一起深入其工作原理

告白数字化转型时代,时速云镌刻价值新起点

In 2022, you still can't "low code"?Data science can also play with Low-Code!

QT MV\MVC structure

你要的七夕文案,已为您整理好!
随机推荐
21天学习挑战赛(2)图解设备树的使用
2022-08-04 第六小组 瞒春 学习笔记
IJCAI2022 | DictBert: Pre-trained Language Models with Contrastive Learning for Dictionary Description Knowledge Augmentation
leetcode - symmetric binary tree
dmp (dump) dump file
torch.roll()
优炫数据库的单节点如何转集群
Bubble Sort and Quick Sort
开发Hololens遇到The type or namespace name ‘HandMeshVertex‘ could not be found..
剑指Offer--找出数组中重复的数字(三种解法)
The second council meeting of the Dragon Lizard Community was successfully held!Director general election, 4 special consultants joined
mysql can't Execute, please solve it
private封装
1527. 患某种疾病的患者
沃谈小知识 |“远程透传”那点事儿
【软件测试】自动化测试之unittest框架
语法基础(变量、输入输出、表达式与顺序语句)
Thinking (88): Use protobuf custom options for multi-version management of data
论治理与创新,2022 开放原子全球开源峰会 OpenAnolis 分论坛圆满落幕
Common open source databases under Linux, how many do you know?