更多ruoyi-nbcio功能请看演示系统
gitee源代码地址
前后端代码: https://gitee.com/nbacheng/ruoyi-nbcio
为了后面流程发起等消息推送,所以需要集成websocket。
1、后端增加websoket支持
首先在framework模块里的pom.xml增加websocket
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
2、增加websocket配置
package com.ruoyi.framework.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * 开启WebSocket支持 */ @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
3、增加websocket服务,当然这部分后面还要修改
package com.ruoyi.framework.websocket; import cn.hutool.json.JSONUtil; import com.ruoyi.common.core.domain.BaseProtocol; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicInteger; // @ServerEndpoint 声明并创建了webSocket端点, 并且指明了请求路径 // id 为客户端请求时携带的参数, 用于服务端区分客户端使用 /** * @ServerEndpoint 声明并创建了websocket端点, 并且指明了请求路径 * uid 为客户端请求时携带的用户id, 用于区分发给哪个用户的消息 * @author nbacheng * @date 2023-09-20 */ @ServerEndpoint("/websocket/{uid}") @Component public class WebSocketServer { // 日志对象 private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class); // 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 //千万不要用++ private static AtomicInteger onlineCount = new AtomicInteger(0); // concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>(); // private static ConcurrentHashMap<String,WebSocketServer> websocketList = new ConcurrentHashMap<>(); // 与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; // 接收uid private String uid = ""; /* * 客户端创建连接时触发 * */ @OnOpen public void onOpen(Session session, @PathParam("uid") String uid) { this.session = session; webSocketSet.add(this); // 加入set中 addOnlineCount(); // 在线数加1 log.info("有新窗口开始监听:" + uid + ", 当前在线人数为" + getOnlineCount()); this.uid = uid; try { sendMessage("连接成功"); } catch (IOException e) { log.error("websocket IO异常"); } } /** * 客户端连接关闭时触发 **/ @OnClose public void onClose() { webSocketSet.remove(this); // 从set中删除 subOnlineCount(); // 在线数减1 log.info("有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 接收到客户端消息时触发 */ @OnMessage public void onMessage(String message, Session session) { log.info("收到来自窗口" + uid + "的信息:" + message); // 群发消息 for (WebSocketServer item : webSocketSet) { try { item.sendMessage(message); } catch (IOException e) { e.printStackTrace(); } } } /** * 连接发生异常时候触发 */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); error.printStackTrace(); } /** * 实现服务器主动推送(向浏览器发消息) */ public void sendMessage(String message) throws IOException { log.info("服务器消息推送:"+message); this.session.getAsyncRemote().sendText(message); } /** * 发送消息到所有客户端 * 指定uid则向指定客户端发消息 * 不指定uid则向所有客户端发送消息 * */ public static void sendInfo(String message, @PathParam("uid") String uid) throws IOException { log.info("推送消息到窗口" + uid + ",推送内容:" + message); for (WebSocketServer item : webSocketSet) { try { // 这里可以设定只推送给这个sid的,为null则全部推送 if (uid == null) { item.sendMessage(message); } else if (item.uid.equals(uid)) { item.sendMessage(message); } } catch (IOException e) { continue; } } } /** * * 给多个指定uid客户端发消息 * * */ public static void sendInfo(String message, @PathParam("uids") String[] uids ) throws IOException { log.info("推送消息到窗口" + uids + ",推送内容:" + message); for (String uid : uids) { sendInfo(message,uid); } } /** * 发送消息到所有客户端 * 指定uid则向指定客户端发消息 * 不指定uid则向所有客户端发送消息 * */ public static void sendInfo(BaseProtocol message, @PathParam("uid") String uid) throws IOException { log.info("推送消息到窗口" + uid + ",推送内容:" + message); for (WebSocketServer item : webSocketSet) { try { // 这里可以设定只推送给这个sid的,为null则全部推送 if (uid == null) { item.sendMessage(JSONUtil.toJsonStr(message)); } else if (item.uid.equals(uid)) { item.sendMessage(JSONUtil.toJsonStr(message)); } } catch (IOException e) { continue; } } } public static synchronized int getOnlineCount() { return onlineCount.get(); } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount.incrementAndGet(); } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount.decrementAndGet(); } public static CopyOnWriteArraySet<WebSocketServer> getWebSocketSet() { return webSocketSet; } }
4、在导航条里增加一个消息
<el-tooltip content="消息" effect="dark" placement="bottom"> <!--<message id="message" class="right-menu-item hover-effect" /> --> <header-notice id="message" class="right-menu-item-message hover-effect" /> </el-tooltip>
界面就是
同时为了样式问题增加下面样式
right-menu-item-message { display: inline-block; padding: 0 8px; height: 100%; font-size: 18px; color: #5a5e66; vertical-align: text-bottom; width: 36px; &.hover-effect { cursor: pointer; transition: background .3s; &:hover { background: rgba(0, 0, 0, .025) } } }
5、增加HeaderNotice 组件,当然现在是测试,只作为websocket消息测试用,后续正式还需要修改。
<template> <div> <a-popover trigger="click" placement="bottomRight" :autoAdjustOverflow="true" :arrowPointAtCenter="true" overlayClassName="header-notice-wrapper" @visibleChange="handleHoverChange" :overlayStyle="{ width: '400px', top: '50px' }"> <template slot="content"> <a-spin :spinning="loadding"> <a-tabs> <a-tab-pane :tab="msg1Title" key="1"> <a-list> <a-list-item :key="index" v-for="(record, index) in announcement1"> <div style="margin-left: 5%;width: 50%"> <p><a @click="showAnnouncement(record)">{{ record.titile }}</a></p> <p style="color: rgba(0,0,0,.45);margin-bottom: 0px">{{ record.createTime }} 发布</p> </div> <div style="text-align: right"> <a-tag @click="showAnnouncement(record)" v-if="record.priority === 'L'" color="blue">一般消息</a-tag> <a-tag @click="showAnnouncement(record)" v-if="record.priority === 'M'" color="orange">重要消息</a-tag> <a-tag @click="showAnnouncement(record)" v-if="record.priority === 'H'" color="red">紧急消息</a-tag> </div> </a-list-item> <div style="margin-top: 5px;text-align: center"> <a-button @click="toMyAnnouncement()" type="dashed" block>查看更多</a-button> </div> </a-list> </a-tab-pane> <a-tab-pane :tab="msg2Title" key="2"> <a-list> <a-list-item :key="index" v-for="(record, index) in announcement2"> <div style="margin-left: 5%;width: 50%"> <p><a @click="showAnnouncement(record)">{{ record.titile }}</a></p> <p style="color: rgba(0,0,0,.45);margin-bottom: 0px">{{ record.createTime }} 发布</p> </div> <div style="text-align: right"> <a-tag @click="showAnnouncement(record)" v-if="record.priority === 'L'" color="blue">一般消息</a-tag> <a-tag @click="showAnnouncement(record)" v-if="record.priority === 'M'" color="orange">重要消息</a-tag> <a-tag @click="showAnnouncement(record)" v-if="record.priority === 'H'" color="red">紧急消息</a-tag> </div> </a-list-item> <div style="margin-top: 5px;text-align: center"> <a-button @click="toMyAnnouncement()" type="dashed" block>查看更多</a-button> </div> </a-list> </a-tab-pane> <a-tab-pane :tab="msg3Title" key="3"> <a-list> <a-list-item :key="index" v-for="(record, index) in announcement3"> <div style="margin-left: 5%;width: 50%"> <p><a @click="showAnnouncement(record)">{{ record.titile }}</a></p> <p style="color: rgba(0,0,0,.45);margin-bottom: 0px">{{ record.createTime }} 发布</p> </div> <div style="text-align: right"> <a-tag @click="showAnnouncement(record)" v-if="record.priority === 'L'" color="blue">一般消息</a-tag> <a-tag @click="showAnnouncement(record)" v-if="record.priority === 'M'" color="orange">重要消息</a-tag> <a-tag @click="showAnnouncement(record)" v-if="record.priority === 'H'" color="red">紧急消息</a-tag> </div> </a-list-item> <div style="margin-top: 5px;text-align: center"> <a-button @click="toMyAnnouncement()" type="dashed" block>查看更多</a-button> </div> </a-list> </a-tab-pane> </a-tabs> </a-spin> </template> <span @click="fetchNotice" class="header-notice"> <a-badge :count="msgTotal"> <a-icon style="font-size: 16px; padding: 4px" type="bell" /> </a-badge> </span> <show-announcement ref="ShowAnnouncement" @ok="modalFormOk"></show-announcement> <dynamic-notice ref="showDynamNotice" :path="openPath" :formData="formData" /> </a-popover> </div> </template> <script> import ShowAnnouncement from './ShowAnnouncement' import store from '@/store/' import DynamicNotice from './DynamicNotice' export default { name: "HeaderNotice", components: { DynamicNotice, ShowAnnouncement, }, data() { return { loadding: false, url: { listCementByUser: "/sys/annountCement/listByUser", editCementSend: "/sys/sysAnnouncementSend/editByAnntIdAndUserId", queryById: "/sys/annountCement/queryById", }, hovered: false, announcement1: [], announcement2: [], announcement3: [], msg1Count: "0", msg2Count: "0", msg3Count: "0", msg1Title: "通知(0)", msg2Title: "", msg3Title: "", stopTimer: false, websock: null, lockReconnect: false, heartCheck: null, formData: {}, openPath: '' } }, computed: { msgTotal() { return parseInt(this.msg1Count) + parseInt(this.msg2Count) + parseInt(this.msg3Count); } }, mounted() { //this.loadData(); //this.timerFun(); this.initWebSocket(); // this.heartCheckFun(); }, destroyed: function() { // 离开页面生命周期函数 this.websocketOnclose(); }, methods: { timerFun() { this.stopTimer = false; let myTimer = setInterval(() => { // 停止定时器 if (this.stopTimer == true) { clearInterval(myTimer); return; } this.loadData() }, 6000) }, loadData() { try { // 获取系统消息 getAction(this.url.listCementByUser).then((res) => { if (res.success) { this.announcement1 = res.result.anntMsgList; this.msg1Count = res.result.anntMsgTotal; this.msg1Title = "通知(" + res.result.anntMsgTotal + ")"; this.announcement2 = res.result.sysMsgList; this.msg2Count = res.result.sysMsgTotal; this.msg2Title = "系统消息(" + res.result.sysMsgTotal + ")"; this.announcement3 = res.result.todealMsgList; this.msg3Count = res.result.todealMsgTotal; this.msg3Title = "待办消息(" + res.result.todealMsgTotal + ")"; } }).catch(error => { console.log("系统消息通知异常", error); //这行打印permissionName is undefined this.stopTimer = true; console.log("清理timer"); }); } catch (err) { this.stopTimer = true; console.log("通知异常", err); } }, fetchNotice() { if (this.loadding) { this.loadding = false return } this.loadding = true setTimeout(() => { this.loadding = false }, 200) }, showAnnouncement(record) { putAction(this.url.editCementSend, { anntId: record.id }).then((res) => { if (res.success) { this.loadData(); } }); this.hovered = false; if (record.openType === 'component') { this.openPath = record.openPage; this.formData = { id: record.busId }; this.$refs.showDynamNotice.detail(record.openPage); } else { this.$refs.ShowAnnouncement.detail(record); } }, toMyAnnouncement() { this.$router.push({ path: '/isps/userAnnouncement' }); }, modalFormOk() {}, handleHoverChange(visible) { this.hovered = visible; }, initWebSocket: function() { // WebSocket与普通的请求所用协议有所不同,ws等同于http,wss等同于https var uid = store.getters.name; var url = process.env.VUE_APP_WS_API + "/websocket/" + uid; console.log("url=",url); this.websock = new WebSocket(url); this.websock.onopen = this.websocketOnopen; this.websock.onerror = this.websocketOnerror; this.websock.onmessage = this.websocketOnmessage; this.websock.onclose = this.websocketOnclose; }, websocketOnopen: function() { console.log("WebSocket连接成功"); //心跳检测重置 //this.heartCheck.reset().start(); }, websocketOnerror: function(e) { console.log("WebSocket连接发生错误"); this.reconnect(); }, websocketOnmessage: function(e) { console.log("-----接收消息-------", e); console.log("-----接收消息-------", e.data); var data = eval("(" + e.data + ")"); //解析对象 if (data.cmd == "topic") { //系统通知 //this.loadData(); this.$notification.open({ //websocket消息通知弹出 message: 'websocket消息通知', description: data.msgTxt, style: { width: '600px', marginLeft: `${335 - 600}px`, }, }); } else if (data.cmd == "user") { //用户消息 //this.loadData(); this.$notification.open({ message: 'websocket消息通知', description: data.msgTxt, style: { width: '600px', marginLeft: `${335 - 600}px`, }, }); } //心跳检测重置 //this.heartCheck.reset().start(); }, websocketOnclose: function(e) { console.log("connection closed (" + e + ")"); if (e) { console.log("connection closed (" + e.code + ")"); } this.reconnect(); }, websocketSend(text) { // 数据发送 try { this.websock.send(text); } catch (err) { console.log("send failed (" + err.code + ")"); } }, openNotification(data) { var text = data.msgTxt; const key = `open${Date.now()}`; this.$notification.open({ message: '消息提醒', placement: 'bottomRight', description: text, key, btn: (h) => { return h('a-button', { props: { type: 'primary', size: 'small', }, on: { click: () => this.showDetail(key, data) } }, '查看详情') }, }); }, reconnect() { var that = this; if (that.lockReconnect) return; that.lockReconnect = true; //没连接上会一直重连,设置延迟避免请求过多 setTimeout(function() { console.info("尝试重连..."); that.initWebSocket(); that.lockReconnect = false; }, 5000); }, heartCheckFun() { var that = this; //心跳检测,每20s心跳一次 that.heartCheck = { timeout: 20000, timeoutObj: null, serverTimeoutObj: null, reset: function() { clearTimeout(this.timeoutObj); //clearTimeout(this.serverTimeoutObj); return this; }, start: function() { var self = this; this.timeoutObj = setTimeout(function() { //这里发送一个心跳,后端收到后,返回一个心跳消息, //onmessage拿到返回的心跳就说明连接正常 that.websocketSend("HeartBeat"); console.info("客户端发送心跳"); //self.serverTimeoutObj = setTimeout(function(){//如果超过一定时间还没重置,说明后端主动断开了 // that.websock.close();//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次 //}, self.timeout) }, this.timeout) } } }, showDetail(key, data) { this.$notification.close(key); var id = data.msgId; getAction(this.url.queryById, { id: id }).then((res) => { if (res.success) { var record = res.result; this.showAnnouncement(record); } }) }, } } </script> <style lang="css"> .header-notice-wrapper { top: 50px !important; } </style> <style lang="less" scoped> .header-notice { display: inline-block; transition: all 0.3s; span { vertical-align: initial; } } </style>
6、增加websocket测试页面,以便测试,地址根据自己需要进行填写
<template> <div> <el-input v-model="url" type="text" style="width: 100%" /> <br /> <el-button @click="join" type="primary">连接</el-button> <el-button @click="exit" type="danger">断开</el-button> <br /> <el-input type="textarea" v-model="message" :rows="9" /> <el-button type="info" @click="send">发送消息</el-button> <br /> <br /> <el-input type="textarea" v-model="text_content" :rows="9" /> 返回内容 <br /> <br /> </div> </template> <script> export default { data() { return { url: "ws://127.0.0.1:9060/websocket/ry", message: "", text_content: "", ws: null, }; }, methods: { join() { const wsuri = this.url; this.ws = new WebSocket(wsuri); const self = this; this.ws.onopen = function (event) { self.text_content = self.text_content + "已经打开连接!" + "\n"; }; this.ws.onmessage = function (event) { self.text_content = event.data + "\n"; }; this.ws.onclose = function (event) { self.text_content = self.text_content + "已经关闭连接!" + "\n"; }; }, exit() { if (this.ws) { this.ws.close(); this.ws = null; } }, send() { if (this.ws) { const messageData = { msgTxt: this.message, cmd: 'user' } let strdata = JSON.stringify(messageData); console.log("strdata",JSON.stringify(messageData)); this.ws.send(strdata); //this.ws.send(this.message); } else { alert("未连接到服务器"); } }, }, }; </script>
7、实际效果图