当前位置:网站首页>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
边栏推荐
- Countdown to 2 days|Cloud native Meetup Guangzhou Station, waiting for you!
- 1873. The special bonus calculation
- Physical backup issues caused by soft links
- Intersection of Boolean Operations in SuperMap iDesktop.Net - Repairing Complex Models with Topological Errors
- 22-07-31周总结
- MRTK3开发Hololens应用-手势拖拽、旋转 、缩放物体实现
- (十一)元类
- Ant Sword Advanced Module Development
- QT language file production
- 告白数字化转型时代,时速云镌刻价值新起点
猜你喜欢
为什么pca分量没有关联
How to sort multiple fields and multiple values in sql statement
21天学习挑战赛(2)图解设备树的使用
Bubble Sort and Quick Sort
Use SuperMap iDesktopX data migration tool to migrate map documents and symbols
Step by step how to perform data risk assessment
人人都在说的数据中台,你需要关注的核心特点是什么?
[Filter tracking] based on matlab unscented Kalman filter inertial navigation + DVL combined navigation [including Matlab source code 2019]
How to Add Category-Specific Widgets in WordPress
论治理与创新,2022 开放原子全球开源峰会 OpenAnolis 分论坛圆满落幕
随机推荐
private封装
Open Source License Description LGPL
倒计时 2 天|云原生 Meetup 广州站,等你来!
北斗三号短报文终端露天矿山高边坡监测方案
In 2022, you still can't "low code"?Data science can also play with Low-Code!
On governance and innovation, the 2022 OpenAtom Global Open Source Summit OpenAnolis sub-forum came to a successful conclusion
Object.defineProperty monitors data changes in real time and updates the page
Review 51 MCU
优炫数据库的单节点如何转集群
Syntax basics (variables, input and output, expressions and sequential statements)
告白数字化转型时代,时速云镌刻价值新起点
Use SuperMap iDesktopX data migration tool to migrate map documents and symbols
Countdown to 2 days|Cloud native Meetup Guangzhou Station, waiting for you!
如何在WordPress中添加特定类别的小工具
MRTK3开发Hololens应用-手势拖拽、旋转 、缩放物体实现
dmp(dump)转储文件
Linux下常见的开源数据库,你知道几个?
开源协议说明LGPL
Details such as compiling pretreatment
Beidou no. 3 short message terminal high slope in open-pit mine monitoring programme