websocket实现在线客服系统
1、后端
先实现一个端点服务。注意:在websocket中导入ChatService时候必须是static的,不然会是null。
@Slf4j
@Component
@ServerEndpoint("/stu/chat/{name}")
public class ChatController {
private static ChatService chatMsgService;
@Autowired
public void setChatService(ChatService chatService) {
ChatController.chatMsgService = chatService;
}
private static final Map<String, Session> clients = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(@PathParam("name") String name, Session session) {
log.info("有新的连接:{}", name);
add(name, session);
log.info("当前在线用户数:{}", clients.size());
}
/**
* 发送消息,前端将消息转成json字符串,后端转成对象
*/
@OnMessage
public void onMessage(String message, Session session) {
ObjectMapper mapper = new ObjectMapper();
try {
ChatMsg msg = mapper.readValue(message, ChatMsg.class);
if ("1".equals(msg.getSendType())) {
// 广播
sendMessageAll(msg.getMsg(), msg.getSendUser());
} else if ("2".equals(msg.getSendType())) {
// 群聊
sendMessageGroup(msg.getMsg(), msg.getMsg());
} else if ("3".equals(msg.getSendType())) {
Session se = clients.get(msg.getAcceptUser());
if (se != null) {
sendMessage(se, msg);
} else {
msg.setIsRead("0");
}
new Thread(() -> {
if (chatMsgService == null) {
log.error("保存聊天消息出错:chatMsgService为null");
return;
}
chatMsgService.addMsg(msg);
}).start();
}
} catch (JsonProcessingException e) {
log.info("发送消息出错");
e.printStackTrace();
}
}
@OnClose
public void onClose(@PathParam("name") String name, Session session) {
log.info("{}:退出聊天", name);
remove(name);
log.info("当前在线用户数:{}", clients.size());
}
@OnError
public void one rror(Session session, Throwable throwable) {
try {
session.close();
} catch (IOException e) {
log.error("退出发生异常: {}", e.getMessage());
}
log.info("连接出现异常: {}", throwable.getMessage());
}
/**
* 新增一个连接
*/
public static void add(String name, Session session) {
if (!name.isEmpty() && session != null) {
clients.put(name, session);
}
}
/**
* 删除一个连接
*/
public static void remove(String name) {
if (!name.isEmpty()) {
clients.remove(name);
}
}
/**
* 获取在线人数
*/
public static int count() {
return clients.size();
}
/**
* 广播
*
* @param message 发送的消息
* @param username 发送人
*/
public static void sendMessageAll(String message, String username) {
log.info("广播消息:{}", message);
clients.forEach((key, session) -> {
if (!username.equals(key)) {
RemoteEndpoint.Async remote = session.getAsyncRemote();
if (remote == null) {
return;
}
remote.sendText(message);
}
});
}
/**
* 群聊
*
* @param message 发送的消息
* @param username 发送人
*/
public static void sendMessageGroup(String message, String username) {
log.info("群发消息");
clients.forEach((key, session) -> {
if (!username.equals(key)) {
RemoteEndpoint.Async remote = session.getAsyncRemote();
if (remote == null) {
return;
}
remote.sendText(message);
}
});
}
/**
* 单聊
*
* @param session session
* @param msg 发送的消息
*/
public static void sendMessage(Session session, ChatMsg msg) {
if (session == null) {
return;
}
final RemoteEndpoint.Async basic = session.getAsyncRemote();
if (basic == null) {
return;
}
String s = JSONArray.toJSON(msg).toString();
basic.sendText(s);
}
}
编写配置文件,如果在系统有spring的定时任务时候,websocket会和spring的定时任务冲突,导致报错,解决办法如下代码。
@Configuration
@EnableWebSocket
public class SocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
// 解决不能同时使用websocket和spring的定时注解
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduling = new ThreadPoolTaskScheduler();
scheduling.setPoolSize(10);
scheduling.initialize();
return scheduling;
}
}
2、前端使用vue
<template>
<section>
<div class="chat-section">
<div class="leftDiv">
<li class="infinite-list-head">
<img class="head-img-size cut-circle" :src="'/api'+userInfo.imgPath"/> {{ userInfo.name }}
</li>
<div class="friends-list">
<ul class="infinite-list">
<li v-for="item in friends" :key="item.id" @click="chat(item)" class="infinite-list-item">
<img class="head-img-size cut-circle" :src="'/api'+item.imgPath"/> {{ item.name }}
</li>
</ul>
</div>
</div>
<div class="chat-div">
<div class="chat-title"><span><img class="head-img-size cut-circle" :src="'/api'+current.imgPath" />{{ current.name }}</span> </div>
<hr/>
<div class="news">
<template v-for="(item,index) in sendMessage">
<!--时间/通知等-->
<div v-if="item.msgType === '10'" class="chat-notice">
<span>{{ timeFormat(item.sendTime) }}</span>
</div>
<!-- 发送的消息 -->
<div v-if="item.sendUser === userInfo.id" class="chat-sender">
<div><img class="cut-circle" :src="'/api'+userInfo.imgPath"/></div>
<div>{{ userInfo.name }}</div>
<div>
<div class="chat-right_triangle"></div>
<span> {{ item.msg }} </span>
</div>
</div>
<!-- 接收的消息 -->
<div v-if="item.acceptUser === userInfo.id && item.sendUser === current.id" class="chat-receiver">
<div><img class="cut-circle" :src="'/api'+current.imgPath"/></div>
<div>{{ current.name }}</div>
<div>
<div class="chat-left_triangle"></div>
<span> {{ item.msg }} </span>
</div>
</div>
</template>
</div>
<hr/>
<div>
<div class="expression">
<!--表情-->
<el-popover placement="top" width="320" trigger="click">
<span v-for="index in 70" :key="index">
<img class="expression-emjio" @click="selectEmjio(index)"
:src="require('@/assets/img/emjio/'+index +'.png')"/>
</span>
<i slot="reference" class="iconfont icon-smiling"></i>
</el-popover>
<i class="iconfont icon-wenjian"></i>
</div>
<div class="send-message">
<el-input
type="textarea"
:rows="2"
placeholder="请输入内容"
class="send-textarea"
v-model="content">
</el-input>
<span><el-button type="primary" class="send-but" @click="sendBut">发 送</el-button></span>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
export default {
name: "chat",
data() {
return {
friends: [],
userInfo: '', // 当前用户信息
current: '', // 当前聊天对象
content: '', // 内容
em: ["惊讶", "难过", "亲亲", "再见", "奋斗", "鄙视", "得意", "坏笑", "调皮", "笑哭", "口吐芬芳", "微笑", "皱眉", "尴尬", "厉害", "吃瓜", "凶狠", "绿帽", "鼓掌", "非洲酋长", "好色", "微笑温暖", "偷笑", "无奈", "疑问", "犯困", "发呆", "有趣", "大哭", "白眼", "衰", "生病", "听歌", "打脸", "摸头", "吐血", "财", "机智", "敲头", "666", "海绵宝宝1", "闭嘴", "可怜", "拒绝", "发火", "害羞1", "无奈流汗", "害羞2", "感动", "胜利", "抓狂", "伤心", "海绵宝宝2", "送爱心", "笑哭2", "晕", "骷髅", "地雷", "击掌", "赞", "握手", "低头", "可爱皱眉", "气爆炸", "呕吐", "惊吓", "吓坏", "亲", "心", "玫瑰"],
sendMessage: [], // 存储消息数组
websocket: '',
}
},
created() {
this.getFriends();
this.initWebSocket();
},
destroyed() {
this.websocket.close();
},
methods: {
// 切换对象
chat(va) {
this.sendMessage = [];
this.content = '';
this.current = va;
},
// 连接websocket
initWebSocket() {
const wsuri = 'ws://localhost:2020/stu/chat/' + this.userInfo.id;
this.websocket = new WebSocket(wsuri);
this.websocket.onmessage = this.websocketonmessage;
},
// 接收数据
websocketonmessage(e) {
let parse = JSON.parse(e.data);
this.sendMessage.push(parse);
},
// 数据发送
websocketsend(agentData) {
this.websocket.send(agentData);
},
// 获取好友列表
async getFriends() {
this.userInfo = JSON.parse(localStorage.getItem("userInfo"));
await this.api.getApi("/chat/friends/get?id=" + this.userInfo.id).then(res => {
this.friends = res.data;
if (this.friends.length > 0) {
this.current = this.friends[0];
}
})
},
// 发送按钮
sendBut() {
if (this.content !== '') {
// 第一条消息之前加上时间
if (this.sendMessage.length === 0) {
this.timeAdd();
}
let msg = {
sendUser: this.userInfo.id,
acceptUser: this.current.id,
msg: this.content,
msgType: '',
sendType: '',
isRead: '1',
sendTime: new Date()
}
// 判断消息类型,发送类型
msg.msgType = '1';
msg.sendType = '3';
this.sendMessage.push(msg);
let s = JSON.stringify(msg);
this.websocketsend(s);
this.content = '';
}
},
// 选择表情
selectEmjio(value) {
let split = this.em[value - 1];
let s = this.content + '[' + split + ']';
this.content = s;
},
// 时间添加
timeAdd() {
let msg = {
sendUser: '',
acceptUser: '',
msg: '',
msgType: '10',
sendType: '',
isRead: '',
sendTime: new Date()
}
this.sendMessage.push(msg);
},
// 时间判断,上一条消息和当前消息差距超过1小时时候显示一条
timeJudgment() {
},
// 时间格式化
timeFormat(te) {
let timedate, s, mm, h, d, m, y, time;
if (te == '') {
return '';
} else if (te.length == 10) {
time = new Date(te * 1000);
y = time.getFullYear();
m = time.getMonth() < 9 ? '0' + (time.getMonth() + 1) : time.getMonth() + 1;
d = time.getDate() < 10 ? '0' + time.getDate() : time.getDate();
h = time.getHours() < 10 ? '0' + time.getHours() : time.getHours();
mm = time.getMinutes() < 10 ? '0' + time.getMinutes() : time.getMinutes();
s = time.getSeconds() < 10 ? '0' + time.getSeconds() : time.getSeconds();
timedate = y + '年' + m + '月' + d + '日 ' + h + ':' + mm + ':' + s;
return timedate;
} else {
time = new Date(te);
y = time.getFullYear();
m = time.getMonth() < 9 ? '0' + (time.getMonth() + 1) : time.getMonth() + 1;
d = time.getDate() < 10 ? '0' + time.getDate() : time.getDate();
h = time.getHours() < 10 ? '0' + time.getHours() : time.getHours();
mm = time.getMinutes() < 10 ? '0' + time.getMinutes() : time.getMinutes();
s = time.getSeconds() < 10 ? '0' + time.getSeconds() : time.getSeconds();
timedate = y + '年' + m + '月' + d + '日 ' + h + ':' + mm + ':' + s;
return timedate;
}
},
// 查询当前消息
findMessage() {
}
}
}
</script>
<style scoped>
.chat-section {
width: 81%;
height: 100%;
margin: 0 auto;
}
/* 左边的列表 */
.leftDiv {
width: 20%;
height: 100.2%;
float: left;
background-color: #2e2e2e;
}
.friends-list {
overflow-y: auto;
overflow-x: hidden;
height: 87%;
}
.friends-list::-webkit-scrollbar {
display: none;
}
.infinite-list {
padding: 0;
margin: 0;
color: white;
list-style-type: none;
}
.infinite-list-head {
color: white;
height: 50px;
margin-top: 1px;
padding: 10px;
display: flex;
justify-content: center;
align-items: center;
background-color: #2e2e2e;
}
.head-img-size {
width: 30px;
height: 30px;
margin-right: 5px;
}
/*头像切园*/
.cut-circle {
border-radius: 50%;
overflow: hidden;
}
.infinite-list-item {
cursor: pointer; /*鼠标放上变手*/
height: 50px;
margin-top: 1px;
padding: 10px;
display: flex;
/*justify-content: center; !*水平居中*!*/
align-items: center; /*垂直居中*/
background-color: #535353;
}
/* 右边内容 */
.chat-div {
width: 79.7%;
height: 100%;
float: left;
border: solid 1px #a3a3a3;
}
.chat-title {
height: 45px;
padding-left: 15px;
display: flex;
align-items: center;
}
.chat-title span {
font-size: 20px;
}
/* 右边中间消息展示 */
.news {
width: 99.7%;
height: 63%;
overflow-y: auto;
overflow-x: hidden;
}
/*隐藏中间的进度条*/
.news::-webkit-scrollbar {
display: none;
}
.expression {
width: 100%;
height: 30px;
padding-left: 15px;
}
.expression-emjio {
width: 35px;
height: 35px;
display: inline-block;
}
.expression i {
font-size: 20px;
color: #7d7d7d;
margin: 0 8px 0 0;
}
.send-message {
}
.send-textarea {
margin-left: 5px;
width: 98%;
}
.send-textarea >>> .el-textarea__inner {
border: 0;
resize: none;
}
.send-message span {
font-size: 13px;
margin: 8px 9px 1px 0;
float: right;
display: block;
color: #aaa9a9;
}
.send-but {
margin: 0;
padding: 0;
height: 25px;
width: 50px;
}
/*接收的消息*/
.chat-receiver {
clear: both;
font-size: 80%;
}
/*聊天气泡效果*/
.chat-receiver div:nth-of-type(1) {
float: left;
}
.chat-receiver div:nth-of-type(2) {
margin: 0 50px 2px 50px;
padding: 0px;
color: #848484;
font-size: 70%;
text-align: left;
}
.chat-receiver div:nth-of-type(3) {
background-color: #27aa95;
margin: 0px 51% 10px 55px;
padding: 10px 10px 10px 10px;
border-radius: 7px;
text-indent: -12px;
width: 41%;
}
/*发送消息*/
.chat-sender {
clear: both;
font-size: 80%;
}
.chat-sender div:nth-of-type(1) {
float: right;
}
.chat-sender div:nth-of-type(2) {
margin: 0px 50px 2px 50px;
padding: 0px;
color: #848484;
font-size: 70%;
text-align: right;
}
.chat-sender div:nth-of-type(3) {
background-color: #b2e281;
margin: 0px 50px 10px 50%;
padding: 10px 10px 10px 10px;
border-radius: 7px;
width: 41%;
}
.chat-receiver div:first-child img,
.chat-sender div:first-child img {
width: 40px;
height: 40px;
/*border-radius: 10%;*/
}
.chat-left_triangle {
height: 0px;
width: 0px;
border-width: 6px;
border-style: solid;
border-color: transparent #27aa95 transparent transparent;
position: relative;
left: -22px;
top: 3px;
}
.chat-right_triangle {
height: 0px;
width: 0px;
border-width: 6px;
border-style: solid;
border-color: transparent transparent transparent #b2e281;
position: relative;
right: -22px;
top: 3px;
}
.chat-notice {
clear: both;
font-size: 70%;
color: white;
text-align: center;
margin-top: 15px;
margin-bottom: 15px;
}
.chat-notice span {
background-color: #cecece;
line-height: 25px;
border-radius: 5px;
padding: 5px 10px;
}
.emjio-img {
width: 30px;
height: 30px;
margin-bottom: -8px;
}
</style>
3、结果