先看效果
1、WebSocket服务建立
1.1 引入包
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>
1.2 新建配置类
package com.ruoyi.web.core.config;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration public class WebSocketConfig {/*** ServerEndpointExporter 作用* <p>* 这个Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint** @return*/@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}}
1.3 新建服务类
package com.ruoyi.web.core.websocket.service;import com.alibaba.fastjson2.JSONObject; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component;import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet;/*** websocket核心代码* // 接口路径 ws://localhost:8080/webSocket/userId;*/ @Component @Slf4j @ServerEndpoint("/webSocket/{userId}") public class WebSocketServer {/*** 与某个客户端的连接会话,需要通过它来给客户端发送数据*/private Session session;/*** 用户ID*/private String userId;/*** concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。* 虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。* 注:底下WebSocket是当前类名*/private static CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();/*** 用来存在线连接用户信息*/private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<String, Session>();private static ApplicationContext applicationContext;public static void setApplicationContext(ApplicationContext context) {applicationContext = context;}/*** 链接成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam(value = "userId") String userId) {try {this.session = session;this.userId = userId;webSockets.add(this);sessionPool.put(userId, session);log.info("【websocket消息】有新的连接,总数为:" + webSockets.size());} catch (Exception e) {}}/*** 链接关闭调用的方法*/@OnClosepublic void onClose() {try {webSockets.remove(this);sessionPool.remove(this.userId);log.info("【websocket消息】连接断开,总数为:" + webSockets.size());} catch (Exception e) {}}/*** 收到客户端消息后调用的方法** @param message*/@OnMessagepublic void onMessage(String message) {log.info("【websocket消息】收到客户端消息:" + message); // JSONObject jsonObject = JSONObject.parseObject(message); }/*** 发送错误时的处理** @param session* @param error*/@OnErrorpublic void onError(Session session, Throwable error) {log.error("用户错误,原因:" + error.getMessage());error.printStackTrace();}/*** 此为广播消息** @param message*/public void sendAllMessage(String message) {log.info("【websocket消息】广播消息:" + message);for (WebSocketServer webSocket : webSockets) {try {if (webSocket.session.isOpen()) {webSocket.session.getAsyncRemote().sendText(message);}} catch (Exception e) {e.printStackTrace();}}}/*** 此为单点消息** @param userId* @param message*/public void sendOneMessage(String userId, String message) {Session session = sessionPool.get(userId);if (session != null && session.isOpen()) {try {log.info("【websocket消息】 单点消息:" + message);session.getAsyncRemote().sendText(message);} catch (Exception e) {e.printStackTrace();}}}/*** 此为单点消息(多人)** @param userIds* @param message*/public void sendMoreMessage(String[] userIds, String message) {for (String userId : userIds) {Session session = sessionPool.get(userId);if (session != null && session.isOpen()) {try {log.info("【websocket消息】 单点消息:" + message);session.getAsyncRemote().sendText(message);} catch (Exception e) {e.printStackTrace();}}}} }
1.4 WebSocket 服务端已经完成,启动项目测试以下是否正常,下面顺便放一个html网页版测试工具
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>wsClient</title><script src="https://code.jquery.com/jquery-3.1.1.min.js"></script><style>.btn-group{display: inline-block;}</style> </head> <body> <input type='text' value='ws://localhost:8080/webSocket/userId' class="form-control" style='width:390px;display:inline'id='wsaddr' /> <div class="btn-group" ><button type="button" class="btn btn-default" onclick='addsocket();'>连接</button><button type="button" class="btn btn-default" onclick='closesocket();'>断开</button><button type="button" class="btn btn-default" onclick='$("#wsaddr").val("")'>清空</button><button type="button" class="btn btn-default" onclick='restore()'>还原</button> </div> <div id="output" style="border:1px solid #ccc;height:365px;overflow: auto;margin: 20px 0;"></div><input type="text" id='message' class="form-control" style='width:810px' placeholder="待发信息" onkeydown="en(event);"><span class="input-group-btn"><button class="btn btn-default" type="button" onclick="doSend();">发送</button></span> </div><script>/*组织时间*/function formatDate(now) {var year = now.getFullYear();var month = now.getMonth() + 1;var date = now.getDate();var hour = now.getHours();var minute = now.getMinutes();var second = now.getSeconds();return year + "-" + (month = month < 10 ? ("0" + month) : month) + "-" + (date = date < 10 ? ("0" + date) : date) +" " + (hour = hour < 10 ? ("0" + hour) : hour) + ":" + (minute = minute < 10 ? ("0" + minute) : minute) + ":" + (second = second < 10 ? ("0" + second) : second);}var output;var websocket;function init() {output = document.getElementById("output");}/*连接按钮*/function addsocket() {var wsaddr = $("#wsaddr").val();if (wsaddr == '') {alert("请填写websocket的地址");return false;}StartWebSocket(wsaddr);}/*断开按钮*/function closesocket() {websocket.close();}/*还原按钮*/function restore(){$("#wsaddr").val('ws://192.168.0.154:8080/');}function en(event) {var evt = evt ? evt : (window.event ? window.event : null);if (evt.keyCode == 13) {doSend()}}/*发送按钮*/function doSend() {var message = $("#message").val();if (message == '') {alert("请先填写发送信息");$("#message").focus();return false;}if (typeof websocket === "undefined") {alert("websocket还没有连接,或者连接失败,请检测");return false;}if (websocket.readyState == 3) {alert("websocket已经关闭,请重新连接");return false;}console.log(websocket);$("#message").val('');writeToScreen('<span style="color:green">你发送的信息 ' + formatDate(new Date()) + '</span><br/>' + message);websocket.send(message);}/*书写内容*/function StartWebSocket(wsUri) {websocket = new WebSocket(wsUri);websocket.onopen = function(evt) {onOpen(evt)};websocket.onclose = function(evt) {onClose(evt)};websocket.onmessage = function(evt) {onMessage(evt)};websocket.onerror = function(evt) {onError(evt)};}function onOpen(evt) {writeToScreen("<span style='color:red'>连接成功,现在你可以发送信息啦!!!</span>");}function onClose(evt) {writeToScreen("<span style='color:red'>websocket连接已断开!!!</span>");websocket.close();}function onMessage(evt) {writeToScreen('<span style="color:blue">服务端回应 ' + formatDate(new Date()) + '</span><br/><span class="bubble">' +evt.data + '</span>');}function onError(evt) {writeToScreen('<span style="color: red;">发生错误:</span> ' + evt.data);}function writeToScreen(message) {var div = "<div class='newmessage'>" + message + "</div>";var d = $("#output");var d = d[0];var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;$("#output").append(div);if (doScroll) {d.scrollTop = d.scrollHeight - d.clientHeight;}}</script> </body> </html>
2、Vue 前端实现
2.1 首先项目中安装speak-tts语音播报插件
npm install speak-tts
2.2创建一个全局的 speech.js文件,文件中引入插件并初始化后导出。因为可能会一直读多条消息,防止初始化多个Speech对象,在全局api中初始化一个对象,方便播报的时候调用。
import Speech from 'speak-tts' const speech=new Speech() export default speech
2.3 在项目点击登录按钮后调用全局的语音播报方法。
由于浏览器之间有安全限制,用户不主动触发语音播报方法, 语音播报不会主动发出声音,故在项目的登录处触发方法。
2.3.1 在登录页面 引入封装好得js文件,并初始化方法
import Speech from '@/utils/speech'initSpeech(){Speech.setLanguage('zh-CN')Speech.init({volume: 0.6, // 音量0-1lang: "zh-CN", // 语言rate: 2, // 语速1正常语速,2倍语速就写2pitch: 1, // 音调voice: "Microsoft Yaoyao - Chinese (Simplified, PRC)",}) }
3 在登录后的页面入口文件处编写弹框样式及告警信息的接收等功能(我项目是elementUI 所以是AppMain.vue 页面 在layout下面)
<template><section class="app-main"><transition name="fade-transform" mode="out-in"><keep-alive :include="cachedViews"><router-view v-if="!$route.meta.link" :key="key" /></keep-alive></transition><iframe-toggle /><div class="alarmmodel" v-if="popupList.length > 0"><el-cardclass="box-card"shadow="always"v-for="(item, index) in popupList":key="index"><div slot="header" class="clearfix"><spanstyle="color: green; font-size: 25px"v-if="item.notifyType == 'newOrder'">{{ index + 1 }}、订单提醒</span><spanstyle="color: red; font-size: 25px"v-else-if="item.notifyType == 'refundOrder'">{{ index + 1 }}、取消订单提醒</span><el-buttonstyle="float: right; padding: 3px 0"type="text"v-if="item.notifyType == 'newOrder'"@click="popupSubmit(item, index, item.notifyType)">接单</el-button><el-buttonstyle="float: right; padding: 3px 0"type="text"v-else@click="popupSubmit(item, index, item.notifyType)">确定</el-button></div><div class="orderInfo"><p><span class="orderInfo_title">名 称:</span> {{ item.goodsName }}</p><p><span class="orderInfo_title">数 量:</span> {{ item.goodsNum }}</p><p><span class="orderInfo_title">金 额:</span> {{ item.payPrice }} 元</p><p><span class="orderInfo_title">备 注:</span> {{ item.remark }}</p></div></el-card></div></section> </template><script> import iframeToggle from "./IframeToggle/index"; import Speech from "@/utils/speech"; import {updateOrderStatus } from "@/api/orderInfo";export default {name: "AppMain",components: { iframeToggle },data() {return {heartbeatTimer: null, // 监测心跳popupList: [], // 存储弹框数据pathpopup: window._CONFIG['WebSocketUrl'], // websocket链接地址socketpopup: null, // 初始化websocket对象 };},computed: {cachedViews() {return this.$store.state.tagsView.cachedViews;},key() {return this.$route.path;},},mounted() {this.popupList = [];this.initPopupsoket();},beforeDestroy() {this.speech.cancel(); // 取消播放this.speech = null;this.socketpopup.onclose = this.closePopup;},methods: {initPopupsoket() {if (typeof WebSocket === "undefined") {alert("您的浏览器不支持socket");} else {//获取当前登录用户IDconst uid = this.$store.getters.id;console.log("当前登录用户信息", uid);if (uid == undefined) {console.log("未获取到商家用户ID,无法实时推送订单消息");} else {this.socketpopup = new WebSocket(this.pathpopup + uid);this.socketpopup.onopen = this.openPopup;this.socketpopup.onerror = this.errorPopup;this.socketpopup.onmessage = this.getMessagepopup;}}},openPopup() {console.log("socketpopup连接成功");this.startHeartbeat(); // 添加心跳监测,用来防止websocket断开 },startHeartbeat() {// 发送心跳消息var _this = this;if (_this.heartbeatTimer == null) {_this.heartbeatTimer = setInterval(function () {console.log("监测心跳");_this.socketpopup.send("ping");}, 10000);}},stopHeartbeat() {// 停止心跳if (this.heartbeatTimer !== null) {clearInterval(this.heartbeatTimer);this.heartbeatTimer = null;}},errorPopup() {console.log("1连接错误");},getMessagepopup(msg) {const returnMsg = JSON.parse(msg.data);console.log("接受消息数据1", returnMsg);console.log("接受消息数据2", returnMsg.message);this.popupList.push(returnMsg); // 将推送的单条数据存起来显示多个弹框,在用户点击确定后消除此条弹框this.startSpeech(returnMsg.message); // 将数据中的告警传给播报的对象 },closePopup() {console.log("socketpopup已经关闭");//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。window.onbeforeunload = function () {this.socketpopup.close();};},startSpeech(text) {Speech.speak({text: text,listeners: {//开始播放onstart: () => {console.log("Start utterance");},//判断播放是否完毕onend: () => {console.log("End utterance");},//恢复播放onresume: () => {console.log("Resume utterance");},},}).then(() => {console.log("读取成功", this.popupList.length);});},popupSubmit(item, index, notifyType) {if (notifyType == "newOrder") {//接单updateOrderStatus({ id: item.orderId, orderStatus: '4' }).then((response) => {this.$message.success("接单成功!");this.popupList.splice(index, 1);this.$router.push({ path: "/order_manage/dl_order_info" }).catch(() => {});});} else {this.popupList.splice(index, 1);}},}, }; </script><style lang="scss" scoped> .app-main {/* 50= navbar 50 */min-height: calc(100vh - 50px);width: 100%;position: relative;overflow: hidden; }.fixed-header + .app-main {padding-top: 50px; }.hasTagsView {.app-main {/* 84 = navbar + tags-view = 50 + 34 */min-height: calc(100vh - 84px);}.fixed-header + .app-main {padding-top: 84px;} } </style><style lang="scss"> // fix css style bug in open el-dialog .el-popup-parent--hidden {.fixed-header {padding-right: 6px;} }::-webkit-scrollbar {width: 6px;height: 6px; }::-webkit-scrollbar-track {background-color: #f1f1f1; }::-webkit-scrollbar-thumb {background-color: #c0c0c0;border-radius: 3px; }.alarmmodel {position: fixed; /* 使div固定在页面上的某个位置 */bottom: 10px; /* 距离顶部10像素 */right: 10px; /* 距离右侧10像素 */z-index: 1000; /* 确保div在其他内容之上 */width: 280px; /* 弹窗宽度 */height: 400px; /* 弹窗高度 */overflow-y: scroll;background-color: rgb(201, 194, 194);color: #fff; /* 文字颜色 */text-align: center; /* 文字居中 */ } .orderInfo {text-align: left; } .orderInfo_title {font-weight: bolder; } </style>