1、公司有个订货的平台,需要开发一个联系客服的及时通讯的功能,之前看了很多文章感觉都有点缺斤少两的,然后自己整合的了,发布一下做记录,如果能帮到你们也挺好
我自己写了个Demo 下面看一下整体的功能和演示,为了防止一些敏感信息,下面的名称显示为用户的ID
webSocket即时通讯
功能实现:
1、两人简单的消息及时发送提醒
2、发送图片
3、发送文件
4、发送emoji表情
5、来消息后有未读提示
前端使用vue实现的
后端是springboot搭建的
前端代码展示:
<template>
<div class="all">
<el-row>
<el-col :span="6" class="left">
<div style="position: fixed;margin-top: 20px;height: 70px;width: 200px;text-align: center">
<div>
<el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"></el-avatar>
</div>
<div>
<el-link type="primary">{{ name }}</el-link>
</div>
</div>
<div style="height: 500px;margin-top: 100px;background-color: #faf6f6">
<GeminiScrollbar autoshow class="my-scroll-bar1">
<ul>
<li v-for="(item,index) in userList" v-if="item.userId != id" :key="index"
@click="btButton(index)"
:style="currentUserId == item.userId ? {
backgroundColor : '#c6c4c4',color: '#4f6edb'
} : {backgroundColor : '#faf6f6'}"
>
<div style="float: left">
<el-badge :value="item.weiDu" :hidden="item.weiDu > 0 ? false : true" class="item" type="success">
<el-image
style="width: 40px; height: 40px;;border-radius: 10px"
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
fit="fit"></el-image>
</el-badge>
</div>
<div style="line-height: 40px;height: 40px;float: left;margin-left: 10px;font-size: 15px">
{{ item.userId }}
</div>
</li>
</ul>
</GeminiScrollbar>
</div>
</el-col>
<el-col :span="18" class="right">
<div style="background-color: #f1f2f3;height: 100%;width: 100%" v-if="toNickName == undefined || aisle == ''">
<div>赶快找个人聊天吧</div>
</div>
<div style="background-color: #f1f2f3;height: 100%;width: 100%" v-else>
<div style="height: 50px;line-height: 50px;font-size: 20px;font-weight: bold;border-bottom: 1px solid black;padding-left: 30px">
{{ toNickName }}
</div>
<div style="height: 550px">
<div id="msgList" style="height: 380px;overflow-y: scroll">
<ul>
<li v-for="(value,index) in messageList" :key="index">
<div>
<div v-if="value.fromId==String(id)" style="width: 100%;text-align: right;margin-top: 10px">
<el-row>
<el-col :span="6">
<br>
</el-col>
<el-col :span="18">
<div style="float: right;width: 10%;margin-right: 10px">
<el-image
style="width: 40px; height: 40px;border-radius: 10px"
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
fit="fit"></el-image>
</div>
<div style="float: right;margin-right: 10px;margin-top: 2px;max-width: 80%">
<div v-if="value.msgType == 0" class="msg_item" style="background-color: #95ec69;color: #4d243c"> {{ value.msg }} </div>
<div v-else-if="value.msgType == 1" class="msg_item">
<image-preview :src="value.msg" style="max-width: 150px"/>
</div>
<div v-else class="msg_item" style="background-color: #b8c6dc">
<div style="width: 150px;height: 30px;line-height: 30px;color: #5e2929">
<span>
<i class="el-icon-folder"/>
{{ value.msg.split('.')[1] }} 文件
</span>
</div>
<div style="width: 150px;height: 30px;line-height: 30px">
<span style="cursor: pointer;float: left" @click="handlePreview(value.msg)"><i class="el-icon-present"/> 在线预览</span>
<span style="cursor: pointer;float: right" @click="fileDown(value.msg)"><i class="el-icon-download"/> 下载</span>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
<div v-else style="width: 100%;margin-top: 10px">
<el-row>
<el-col :span="18">
<div style="float: left;width: 10%;margin-left: 10px">
<el-image
style="width: 40px; height: 40px;border-radius: 10px"
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
fit="fit"></el-image>
</div>
<div style="float: left;margin-left: 10px;margin-top: 2px;max-width: 80%">
<div v-if="value.msgType == 0" class="msg_item" style="background-color: #ffffff;color: #4d243c"> {{ value.msg }} </div>
<div v-else-if="value.msgType == 1" class="msg_item">
<image-preview :src="value.msg" style="max-width: 150px"/>
</div>
<div v-else class="msg_item" style="background-color: #b8c6dc">
<div style="width: 150px;height: 30px;line-height: 30px;color: #5e2929">
<span>
<i class="el-icon-folder"/>
{{ value.msg.split('.')[1] }} 文件
</span>
</div>
<div style="width: 150px;height: 30px;line-height: 30px">
<span style="cursor: pointer;float: left" @click="handlePreview(value.msg)"><i class="el-icon-present"/> 在线预览</span>
<span style="cursor: pointer;float: right" @click="fileDown(value.msg)"><i class="el-icon-download"/> 下载</span>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</li>
</ul>
</div>
<div style="height: 20px">
</div>
<div style="height: 150px;">
<el-input type="textarea"
:rows="5"
placeholder="请输入内容" v-model="messageValue"
@keyup.ctrl.enter.native="sendMsg"
></el-input>
<el-row>
<el-col :span="5">
<el-popover
placement="bottom-start"
width="500"
trigger="click">
<div style="height: 100px;overflow-y: scroll">
<div v-for="(item,index) in emojiList"
style="margin: 5px;background-color: #d9d9d9;
width: 50px;height: 50px;float: left;
text-align: center;line-height: 50px;
cursor: pointer;" @click="addEmoji(item.icon)">
{{ item.icon }}
</div>
</div>
<el-button slot="reference" size="mini">表情</el-button>
</el-popover>
</el-col>
<el-col :span="5" style="line-height: 30px">
<br>
</el-col>
<el-col :span="5" style="line-height: 30px">
<el-tooltip content="发送图片" effect="light" placement="bottom-start">
<i class="el-icon-picture-outline" @click="uploadPhoto">
<input ref="uploadPhoto" type="file" accept="image/*" @change="getPhoto" style="display: none"/>
</i>
</el-tooltip>
</el-col>
<el-col :span="5" style="line-height: 30px">
<el-tooltip content="发送文件" effect="light" placement="bottom-start">
<i class="el-icon-folder-opened" @click="uploadFile">
<input ref="uploadFile" type="file" accept="*/*" @change="getFile" style="display: none"/>
</i>
</el-tooltip>
</el-col>
<el-col :span="4" style="text-align: right">
<el-tooltip content="ctrl+回车快捷发送" effect="light" placement="bottom-start">
<el-button size="mini" type="success" @click="sendMsg">发送</el-button>
</el-tooltip>
</el-col>
</el-row>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
data() {
return {
id: "", // id
name: "", // 昵称
websocket: null, // WebSocket对象
messageList: [],//消息集合
userList:[],//用户列表集合
currentUserId: -1,
currentIndex: -1,
toNickName: null,
aisle: "", //对方频道号
messageValue: "",//文本框消息
}
},
methods: {
//初始化
connectWebSocket(){
//查看浏览器是否兼容WebSocket
if ("WebSocket" in window) {
this.websocket = new WebSocket(
"ws://localhost:8877/websocket/" + this.id + "/"+this.name
);
} else {
alert("不支持建立socket连接");
}
//连接发生错误的回调方法
this.websocket.onerror = function() {
this.$message({
type: 'error',
message: '发生错误啦,消息发送失败哦'
})
};
//连接成功建立的回调方法
this.websocket.onopen = function(event) {
};
//接收到消息的回调方法
var that = this;
this.websocket.onmessage = function(event) {
console.log(event)
var object = eval("(" + event.data + ")");
//获取用户
if (object.type == 0) {
that.userList = object.userList
console.log(that.userList)
}
//接受消息
if (object.type == 1) {
if (that.currentIndex == -1) {
that.userList = object.users
}else {
that.userList = object.users;
if (object.msg.fromId == that.currentUserId){
//如果是在当前聊天对话框,这修改消息为已读
updateMsgStat(object.msg.fromId,object.msg.toId).then(res => {
that.userList[0].weiDu = 0
that.messageList.push(object.msg)
that.rollScroll();
})
}
}
}
};
//连接关闭的回调方法
this.websocket.onclose = function() {};
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function() {
this.websocket.close();
};
},
//滚动到底部 点击用户名称打开聊天窗口默认滚到底部
rollScroll(){
this.$nextTick(() => {
let msg = document.getElementById('msgList') // 获取对象
msg.scrollTop = msg.scrollHeight;
})
},
//发送消息接口
sendMessage(msg){
msg.type = 1;
this.messageList.push(msg);//将本次的发送的消息添加到消息列表中
this.websocket.send(JSON.stringify(msg));//通过webSocket将消息对象发送,消息对象中包{消息类型,消息内容,发送对象ID,接收对象ID等} 可自行设置
this.rollScroll();
},
//点击单个用户查询与该用户的聊天记录
btButton(index){
this.currentUserId = this.userList[index].userId
this.currentIndex = index
this.toNickName = this.userList[index].userId
this.aisle = this.userList[index].userId
this.userList[index].weiDu = 0
//点击时查询与该用户的聊天记录
selectMsg(this.id,this.userList[index].userId).then(res => {
this.messageList = res.data
this.rollScroll();
})
},
//发送文本数据
sendMsg(){
var socketMsg = { msg: this.messageValue,fromId: this.id, toId: this.aisle,msgType: "0"};
if(this.messageValue==""){
return this.$message({
type: 'info',
message: '发送内容不能未空哦~'
})
}
this.sendMessage(socketMsg);//调用上面发送消息方法
this.messageValue = ""
},
//发送文件和图片的思想就是,将文件和图片上传至服务器然后返回文件链接,将链接作为消息内容存入数据库中进行保存和发送
// 发送图片数据
getPhoto(event) {
//这就是你上传的文件
this.fileList = event.target.files;
let files = Array.from(this.fileList);
files.forEach(item => {
let formData = new FormData();
formData.append('file', item);
console.log(formData)
sendFile(formData).then(res => {
console.log(res)
var socketMsg = { msg: res.url,fromId: this.id, toId: this.aisle,msgType: "1"};
this.sendMessage(socketMsg)//调用上面发送消息方法
})
})
},
// 发送文件数据
getFile(event) {
//这就是你上传的文件
this.fileList = event.target.files;
let files = Array.from(this.fileList);
files.forEach(item => {
let formData = new FormData();
formData.append('file', item);
console.log(formData)
sendFile(formData).then(res => {
console.log(res)
var socketMsg = { msg: res.url,fromId: this.id, toId: this.aisle,msgType: "2"};
this.sendMessage(socketMsg)//调用上面发送消息方法
})
})
},
//添加表情到文本域 点击emoji然后将点击emoji加入到文本域中即可
addEmoji(emoji){
this.messageValue += emoji
},
}
//css样式
<style scoped lang="scss">
.home {
blockquote {
padding: 10px 20px;
margin: 0 0 20px;
font-size: 17.5px;
border-left: 5px solid #eee;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #eee;
}
.col-item {
margin-bottom: 20px;
}
ul {
padding: 0;
margin: 0;
}
font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
color: #676a6c;
overflow-x: hidden;
ul {
list-style-type: none;
}
h4 {
margin-top: 0px;
}
h2 {
margin-top: 10px;
font-size: 26px;
font-weight: 100;
}
p {
margin-top: 10px;
b {
font-weight: 700;
}
}
.update-log {
ol {
display: block;
list-style-type: decimal;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0;
margin-inline-end: 0;
padding-inline-start: 40px;
}
}
.main {
position: relative;
top: 20px;
}
.message {
position: relative;
overflow:auto;
width: 100%;
height: 40%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
padding: 5px;
}
.all {
width: 800px;
margin: auto;
background-color: #e2c7b5;
}
.left {
height: 600px;
}
.right {
height: 600px
}
.left li {
height: 50px;
line-height: 50px;
padding: 5px;
cursor: pointer;
clear: left;
}
.left li:hover {
height: 50px;
border: 1px dotted #c29254;
border-radius: 10px;
}
.right li {
min-height: 40px;
width: 100%;
}
.msg_item {
line-height: 30px;
padding: 5px 10px;
border-radius: 5px;
text-align: left
}
.my-scroll-bar1 {
height: 500px;
}
#msgList::-webkit-scrollbar {
width: 5px;
height: 5px;
}
#msgList::-webkit-scrollbar-track {
background-color: #f1f2f3;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius:2em;
}
#msgList::-webkit-scrollbar-thumb {
background-color: rgba(28, 28, 203, 0.5);
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius:2em;
}
}
</style>
后端代码展示:
展示前所需准备:
1、添加webSocket依赖:
<!--websocket依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、创建一个webSocket配置类:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author lc
* @date 2022/12/8
* @email 1669754469@qq.com
* @description WebSocketConfig配置类,注入对象ServerEndpointExporter,
* * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3、如有拦截器或者过滤器需要将刚刚前端连接的接口过滤掉,不然可能会请求报错
4、websocket接口类
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.domain.SocketMsg;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.service.SocketMsgService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.json.JsonParseException;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import springfox.documentation.spring.web.json.Json;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @author lc
* @date 20221208
* @description websocket的具体实现类
* 使用springboot的唯一区别是要@Component声明,而使用独立容器是由容器自己管理websocket的,
* 但在springboot中连容器都是spring管理的。
* 虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,
* 所以可以用一个静态set保存起来
*/
@ServerEndpoint(value = "/websocket/{id}/{nickName}")
@Component
public class WebSocketController {
private String id;
private String nickName;
private Session session;
//用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<WebSocketController> webSocketSet = new CopyOnWriteArraySet<WebSocketController>();
private static Map<String,SysUser> users = new HashMap<>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
//用来记录sessionId和该session进行绑定
private static Map<String, Session> map = new HashMap<String, Session>();
private static SocketMsgService socketMsgService;
@Autowired
public void setChatService(SocketMsgService chatService) {
WebSocketController.socketMsgService = chatService;
}
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("id") String id,@PathParam("nickName") String nickName) {
Map<String,Object> message=new HashMap<String, Object>();
this.session = session;
this.id = id;
this.nickName = nickName;
map.put(id, session);
webSocketSet.add(this);//加入set中
System.out.println("有新连接加入:" + nickName + ",当前在线人数为" + webSocketSet.size());
//查询接受者的消息列表
List<SysUser> users = socketMsgService.sekectMsgList(id);
message.put("type",0); //消息类型,0-连接成功,1-用户消息,2-上线提醒
message.put("people",webSocketSet.size()); //在线人数
message.put("name",nickName); //昵称
message.put("aisle",id); //频道号
message.put("userList",users); //用户集合
this.session.getAsyncRemote().sendText(JSONObject.toJSONString(message));
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); //从set中删除
users.remove(this.id);
System.out.println("有一连接关闭!当前在线人数为" + webSocketSet.size());
}
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session, @PathParam("id") String id,@PathParam("nickName") String nickName) {
System.out.println("来自客户端的消息-->" + nickName + ": " + message);
//从客户端传过来的数据是json数据,所以这里使用jackson进行转换为SocketMsg对象,
// 然后通过socketMsg的type进行判断是单聊还是群聊,进行相应的处理:
ObjectMapper objectMapper = new ObjectMapper();
SocketMsg socketMsg;
try {
socketMsg = objectMapper.readValue(message, SocketMsg.class);
if (socketMsg.getType() == 1) {
//单聊.需要找到发送者和接受者.
socketMsg.setFromId(id);//发送者.
socketMsg.setCreateTime(new Date());
Session fromSession = map.get(socketMsg.getFromId());
Session toSession = map.get(socketMsg.getToId());
socketMsgService.insertMsg(socketMsg);
//发送给接受者.
if (toSession != null) {
//查询接受者的消息列表
List<SysUser> users = socketMsgService.sekectMsgList(socketMsg.getToId());
//发送给发送者.
Map<String,Object> m=new HashMap<String, Object>();
m.put("type","1");
m.put("msg",socketMsg);
m.put("users",users);
toSession.getAsyncRemote().sendText(JSONObject.toJSONString(m));
} else {
socketMsg.setMsg("不好意思,对方不在线");
socketMsg.setMsgType("0");
//发送给发送者.
Map<String,Object> m=new HashMap<String, Object>();
m.put("type","99");
m.put("msg",socketMsg);
m.put("users",users);
fromSession.getAsyncRemote().sendText(JSONObject.toJSONString(m));
}
} else {
//群发消息
broadcast(nickName + ": " + socketMsg.getMsg());
}
} catch (JsonParseException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
session.getAsyncRemote().sendText("发生错误啦,消息发送失败哦");
error.printStackTrace();
}
/**
* 群发自定义消息
*/
public void broadcast(String message) {
for (WebSocketController item : webSocketSet) {
item.session.getAsyncRemote().sendText(message);//异步发送消息.
}
}
}
解释:
@ServerEndpoint(value = "/websocket/{id}/{nickName}")
这块代码必须和前端初始化连接一致,否则连接不成功
4个注解:
//1、初始化连接时会调用此注解下的方法 需要将当前连接对象的信息存入webSocketSet对象中
@OnOpen
//2、关闭连接时触发,将当前对象的信息从webSocketSet中删除
@OnClose
//3、接收到客户端发送的消息
@OnMessage
//4、发送异常时调用
@OnError
大致的功能就是这样,如果能帮助到大家就更好了