1、前端引入websocket

2、实现预警数据通过websocket统计在右上角
3、使用redis记录websocket客户端用户
pull/254/head
xjs 4 years ago
parent 472af60970
commit 05cd60971c

@ -164,6 +164,25 @@ public class RedisService {
return redisTemplate.opsForSet().members(key);
}
/**
* set
* @param key redis
* @param value set
* @param <T> obj
* @return
*/
public <T> Long removeSet(String key, T value) {
Long size = null;
try {
size = redisTemplate.opsForSet().remove(key, value);
} catch (Exception e) {
return size;
}
return size;
}
/**
* Map
*

@ -7,6 +7,7 @@ import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.lang.annotation.*;
@ -24,6 +25,7 @@ import java.lang.annotation.*;
@Import({ ApplicationConfig.class, FeignAutoConfiguration.class })
//自定义bean扫描添加xjs路径下的bean
@ComponentScan(basePackages = {"com.ruoyi","com.xjs"})
@EnableTransactionManagement
public @interface EnableCustomConfig
{

@ -4,7 +4,7 @@ VUE_APP_TITLE = 管理平台
# 开发环境配置
ENV = 'development'
# 若依管理系统/开发环境
# 管理系统/开发环境
VUE_APP_BASE_API = '/dev-api'
# 路由懒加载

@ -1,28 +1,34 @@
<template>
<div class="navbar">
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container"
@toggleClick="toggleSideBar"/>
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav"/>
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>
<div class="right-menu">
<template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item" />
<el-badge :value="warnData.count" class=" hover-effect share-button">
<el-button type="warning" icon="el-icon-check" circle style="max-width: 22px;max-height: 22px;"></el-button>
</el-badge>
<!--todo 右上角添加信息提示等功能-->
<template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item"/>
<screenfull id="screenfull" class="right-menu-item hover-effect" />
<screenfull id="screenfull" class="right-menu-item hover-effect"/>
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
<size-select id="size-select" class="right-menu-item hover-effect"/>
</el-tooltip>
</template>
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
<div class="avatar-wrapper">
<img :src="avatar" class="user-avatar">
<i class="el-icon-caret-bottom" />
<i class="el-icon-caret-bottom"/>
</div>
<el-dropdown-menu slot="dropdown">
<router-link to="/user/profile">
@ -41,7 +47,7 @@
</template>
<script>
import { mapGetters } from 'vuex'
import {mapGetters} from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
@ -62,11 +68,19 @@ export default {
RuoYiGit,
RuoYiDoc
},
data() {
return {
warnData: {},
}
},
computed: {
...mapGetters([
'sidebar',
'avatar',
'device'
'device',
"$socket",
]),
setting: {
get() {
@ -85,7 +99,21 @@ export default {
}
}
},
mounted() {
this.$socket.registerCallBack(
"apiWarning",
this.getData
);
},
methods: {
getData(data) {
if (data) {
this.warnData = data
}
},
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
@ -98,19 +126,41 @@ export default {
this.$store.dispatch('LogOut').then(() => {
location.href = '/index';
})
}).catch(() => {});
}).catch(() => {
});
}
}
},
beforeDestroy() {
// ,
this.$socket.unRegisterCallBack();
},
}
</script>
<style lang="scss" scoped>
.share-button {
margin-right: 23px;
color: #5a5e66;
padding-bottom: 22px;
&.hover-effect {
cursor: pointer;
transition: background .3s;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
}
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
.hamburger-container {
line-height: 46px;
@ -118,7 +168,7 @@ export default {
float: left;
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color:transparent;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, .025)

@ -1,107 +1,128 @@
<template>
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar class="sidebar-container"/>
<div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar />
<tags-view v-if="needTagsView" />
</div>
<app-main />
<right-panel>
<settings />
</right-panel>
</div>
</div>
</template>
<script>
import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
import variables from '@/assets/styles/variables.scss'
export default {
name: 'Layout',
components: {
AppMain,
Navbar,
RightPanel,
Settings,
Sidebar,
TagsView
},
mixins: [ResizeMixin],
computed: {
...mapState({
theme: state => state.settings.theme,
sideTheme: state => state.settings.sideTheme,
sidebar: state => state.app.sidebar,
device: state => state.app.device,
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader
}),
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
},
variables() {
return variables;
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/assets/styles/mixin.scss";
@import "~@/assets/styles/variables.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.mobile .fixed-header {
width: 100%;
}
</style>
<template>
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar class="sidebar-container"/>
<div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar/>
<tags-view v-if="needTagsView"/>
</div>
<app-main/>
<right-panel>
<settings/>
</right-panel>
</div>
</div>
</template>
<script>
import RightPanel from '@/components/RightPanel'
import {AppMain, Navbar, Settings, Sidebar, TagsView} from './components'
import ResizeMixin from './mixin/ResizeHandler'
import {mapState} from 'vuex'
import variables from '@/assets/styles/variables.scss'
import SocketService from "@/utils/socket-server";
export default {
name: 'Layout',
components: {
AppMain,
Navbar,
RightPanel,
Settings,
Sidebar,
TagsView
},
mixins: [ResizeMixin],
computed: {
...mapState({
theme: state => state.settings.theme,
sideTheme: state => state.settings.sideTheme,
sidebar: state => state.app.sidebar,
device: state => state.app.device,
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader
}),
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
},
variables() {
return variables;
}
},
created() {
this.connectWebsocket();
},
methods: {
//websocket
connectWebsocket() {
if (!this.$socket) {
SocketService.Instance.connect();
this.$store.dispatch("app/set$Socket", SocketService.Instance);
}
},
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', {withoutAnimation: false})
}
},
beforeDestroy() {
if (this.$socket) {
this.$socket.closeWebsocket();
this.$store.dispatch("app/set$Socket", null);
}
},
}
</script>
<style lang="scss" scoped>
@import "~@/assets/styles/mixin.scss";
@import "~@/assets/styles/variables.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.mobile .fixed-header {
width: 100%;
}
</style>

@ -1,18 +1,19 @@
const getters = {
sidebar: state => state.app.sidebar,
size: state => state.app.size,
device: state => state.app.device,
visitedViews: state => state.tagsView.visitedViews,
cachedViews: state => state.tagsView.cachedViews,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
introduction: state => state.user.introduction,
roles: state => state.user.roles,
permissions: state => state.user.permissions,
permission_routes: state => state.permission.routes,
topbarRouters:state => state.permission.topbarRouters,
defaultRoutes:state => state.permission.defaultRoutes,
sidebarRouters:state => state.permission.sidebarRouters,
}
export default getters
const getters = {
sidebar: state => state.app.sidebar,
size: state => state.app.size,
device: state => state.app.device,
visitedViews: state => state.tagsView.visitedViews,
cachedViews: state => state.tagsView.cachedViews,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
introduction: state => state.user.introduction,
roles: state => state.user.roles,
permissions: state => state.user.permissions,
permission_routes: state => state.permission.routes,
topbarRouters:state => state.permission.topbarRouters,
defaultRoutes:state => state.permission.defaultRoutes,
sidebarRouters:state => state.permission.sidebarRouters,
$socket: state => state.app.$socket,
}
export default getters

@ -1,56 +1,64 @@
import Cookies from 'js-cookie'
const state = {
sidebar: {
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
withoutAnimation: false
},
device: 'desktop',
size: Cookies.get('size') || 'medium'
}
const mutations = {
TOGGLE_SIDEBAR: state => {
state.sidebar.opened = !state.sidebar.opened
state.sidebar.withoutAnimation = false
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1)
} else {
Cookies.set('sidebarStatus', 0)
}
},
CLOSE_SIDEBAR: (state, withoutAnimation) => {
Cookies.set('sidebarStatus', 0)
state.sidebar.opened = false
state.sidebar.withoutAnimation = withoutAnimation
},
TOGGLE_DEVICE: (state, device) => {
state.device = device
},
SET_SIZE: (state, size) => {
state.size = size
Cookies.set('size', size)
}
}
const actions = {
toggleSideBar({ commit }) {
commit('TOGGLE_SIDEBAR')
},
closeSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
toggleDevice({ commit }, device) {
commit('TOGGLE_DEVICE', device)
},
setSize({ commit }, size) {
commit('SET_SIZE', size)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
import Cookies from 'js-cookie'
const state = {
sidebar: {
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
withoutAnimation: false
},
device: 'desktop',
size: Cookies.get('size') || 'medium',
// websocket实例
$socket: null
}
const mutations = {
TOGGLE_SIDEBAR: state => {
state.sidebar.opened = !state.sidebar.opened
state.sidebar.withoutAnimation = false
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1)
} else {
Cookies.set('sidebarStatus', 0)
}
},
CLOSE_SIDEBAR: (state, withoutAnimation) => {
Cookies.set('sidebarStatus', 0)
state.sidebar.opened = false
state.sidebar.withoutAnimation = withoutAnimation
},
TOGGLE_DEVICE: (state, device) => {
state.device = device
},
SET_SIZE: (state, size) => {
state.size = size
Cookies.set('size', size)
},
SET_$SOCKET: (state, socket) => {
state.$socket = socket
}
}
const actions = {
toggleSideBar({commit}) {
commit('TOGGLE_SIDEBAR')
},
closeSideBar({commit}, {withoutAnimation}) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
toggleDevice({commit}, device) {
commit('TOGGLE_DEVICE', device)
},
setSize({commit}, size) {
commit('SET_SIZE', size)
},
set$Socket({commit}, socket) {
commit('SET_$SOCKET', socket)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

@ -0,0 +1,134 @@
import { Message } from "element-ui";
import store from '@/store'
// let wsUrl = 'ws://127.0.0.1:9002/warning/api';
export default class SocketService {
/**
* 单例
*/
static instance = null;
static get Instance() {
if (!this.instance) {
this.instance = new SocketService();
}
return this.instance;
}
// 和服务端连接的socket对象
ws = null;
// 存储回调函数
callBackMapping = {};
// 标识是否连接成功
connected = false;
// 记录重试的次数
sendRetryCount = 0;
// 重新连接尝试的次数
connectRetryCount = 0;
// 是否手动关闭websocket连接
isManualClose = false
// 定义连接服务器的方法
connect() {
// 连接服务器
if (!window.WebSocket) {
return console.log("您的浏览器不支持WebSocket");
}
//网关转发
let wsUrl = 'ws://localhost:8080/warning/warning/api';
wsUrl += `/${store.getters.name}`
this.ws = new WebSocket(wsUrl)
// 连接成功的事件
this.ws.onopen = () => {
console.log("连接服务端成功了");
this.connected = true;
// 重置重新连接的次数
this.connectRetryCount = 0;
};
// 1.连接服务端失败
// 2.当连接成功之后, 服务器关闭的情况
this.ws.onclose = (event) => {
// e.code === 1000 表示正常关闭。 无论为何目的而创建, 该链接都已成功完成任务。
// e.code !== 1000 表示非正常关闭。
console.log("onclose event: ", event);
this.connected = false;
if (event && event.code !== 1000) {
console.log("连接服务端失败");
// 如果不是手动关闭,这里的重连会执行;如果调用了手动关闭函数,这里重连不会执行
this.connectRetryCount++;
setTimeout(() => {
this.connect();
}, 500 * this.connectRetryCount);
}
};
// 得到服务端发送过来的数据
this.ws.onmessage = (msg) => {
console.log("从服务端获取到了数据");
// 真正服务端发送过来的原始数据时在msg中的data字段
const recvData = JSON.parse(msg.data);
console.log(recvData)
const socketType = recvData.socketType;
if (this.callBackMapping[socketType]){
this.callBackMapping[socketType].call(this, recvData);
}
};
}
// 回调函数的注册
registerCallBack(socketType, callBack) {
this.callBackMapping[socketType] = callBack;
}
// 取消某一个回调函数
unRegisterCallBack(socketType) {
this.callBackMapping[socketType] = null;
}
// 发送数据的方法
send(data) {
// 判断此时此刻有没有连接成功
if (this.connected) {
this.sendRetryCount = 0;
this.ws.send(JSON.stringify(data));
console.log('xxxxxxxxxxxx')
} else if (!this.isManualClose) {
console.log('=============')
this.sendRetryCount++;
setTimeout(() => {
this.send(data);
}, this.sendRetryCount * 500);
}
}
// 手动关闭websocket 这里手动关闭会执行onclose事件
closeWebsocket() {
if (this.ws) {
try {
this.ws.close(); // 关闭websocket
this.ws = null
this.callBackMapping = {}
this.connected = false
this.sendRetryCount = 0
this.connectRetryCount = 0
this.isManualClose = true
this.instance = null
SocketService.instance = null
} catch (error) {
console.log(error)
}
}
}
}
const MessageEvent = { // onmessage回调函数的event
data: { // websocket通讯接收到的数据
socketType: '', // websocket双方通讯的名称
data: {}, // 返回的真实数据
}
}

@ -24,6 +24,11 @@ public class RedisConst {
*/
public static final String HOT = "hot";
/**
* websocketkey
*/
public static final String WEBSOCKET= "WEBSOCKET";
//-------------------有效时间-----------------------
public static final Integer TRAN_DICT_EXPIRE = 7; //天

@ -1,5 +1,6 @@
package com.xjs.controller;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.poi.ExcelUtil;
@ -8,16 +9,23 @@ import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.TableDataInfo;
import com.ruoyi.common.log.annotation.Log;
import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import com.xjs.domain.ApiRecord;
import com.xjs.domain.ApiWarning;
import com.xjs.server.WebSocketServer;
import com.xjs.service.ApiWarningService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import static com.xjs.consts.RedisConst.WEBSOCKET;
/**
* @author xiejs
@ -30,6 +38,8 @@ public class ApiWarningController extends BaseController {
@Autowired
private ApiWarningService apiWarningService;
@Autowired
private RedisService redisService;
/**
* apiRecord
@ -66,17 +76,41 @@ public class ApiWarningController extends BaseController {
}
/**
* api
* apiwebsocket
*
* @param apiWarning
* @return R
*/
@PostMapping("saveApiwarningForRPC")
@Transactional
public R<ApiWarning> saveApiWarningForRPC(@RequestBody ApiWarning apiWarning) {
boolean save = apiWarningService.save(apiWarning);
this.websocketPush(apiWarning);
return save ? R.ok() : R.fail();
}
/**
* websocket
*/
private void websocketPush(ApiWarning apiWarning) {
long count = apiWarningService.count();
Set<String> cacheSet = redisService.getCacheSet(WEBSOCKET);
JSONObject jsonData =new JSONObject();
JSONObject jsonObject = (JSONObject) JSONObject.toJSON(apiWarning);
jsonData.put("count", count);
jsonData.put("data", jsonObject.toJSONString());
jsonData.put("socketType", "apiWarning");
for (String userId : cacheSet) {
try {
WebSocketServer.sendInfo(jsonData.toString(),userId);
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}
/**
* api

@ -1,15 +1,23 @@
package com.xjs.server;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.redis.service.RedisService;
import com.xjs.service.ApiWarningService;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
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.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static com.xjs.consts.RedisConst.WEBSOCKET;
/**
* apiwebsocket
*
@ -17,9 +25,25 @@ import java.util.concurrent.ConcurrentHashMap;
* @since 2022-01-13
*/
@Log4j2
@ServerEndpoint("warning/api/{userId}")
@ServerEndpoint("/warning/api/{userId}")
@Component
public class WebSocketServer {
/**
* setWebSocketServerset
*/
private static RedisService redisService;
@Autowired
public void setRedisService(RedisService redisService) {
WebSocketServer.redisService = redisService;
}
private static ApiWarningService apiWarningService;
@Autowired
public void setApiWarningService(ApiWarningService apiWarningService) {
WebSocketServer.apiWarningService = apiWarningService;
}
/**
* 线线
*/
@ -56,12 +80,21 @@ public class WebSocketServer {
}
log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount());
Set<String> set = new HashSet<>();
set.add(userId);
redisService.setCacheSet(WEBSOCKET, set);
long count = apiWarningService.count();
JSONObject jsonData =new JSONObject();
jsonData.put("count", count);
jsonData.put("socketType", "apiWarning");
jsonData.put("data", "{}");
try {
sendMessage("连接成功");
sendMessage(jsonData.toJSONString());
} catch (IOException e) {
log.error("用户:" + userId + ",网络异常!!!!!!");
e.printStackTrace();
}
}
/**
@ -73,6 +106,9 @@ public class WebSocketServer {
webSocketMap.remove(userId);
//从set中删除
subOnlineCount();
//退出移出用户
redisService.removeSet(WEBSOCKET, this.userId);
}
log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount());
}
@ -107,6 +143,7 @@ public class WebSocketServer {
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
redisService.removeSet(WEBSOCKET, this.userId);
error.printStackTrace();
}

Loading…
Cancel
Save