!62 新增监控在线用户

* 去掉durid广告
* 新增监控在线用户功能
* 新增监控在线用户功能
This commit is contained in:
二世阿博 2021-06-11 20:03:21 +08:00 committed by JEECG开源社区
parent 1183dd0a32
commit 5889fc8fac
7 changed files with 443 additions and 0 deletions

View File

@ -71,4 +71,17 @@ export function thirdLogin(token,thirdType) {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
/**
* 强退其他账号
* @param token
* @returns {*}
*/
export function forceLogout(parameter) {
return axios({
url: '/sys/online/forceLogout',
method: 'post',
data: parameter
})
}

View File

@ -0,0 +1,158 @@
<template>
<a-card :bordered="false">
<!-- 查询区域 -->
<div class="table-page-search-wrapper">
<a-form layout="inline" @keyup.enter.native="searchQuery">
<a-row :gutter="24">
<a-col :md="6" :sm="12">
<a-form-item label="账号">
<a-input placeholder="请输入账号查询" v-model="queryParam.username"></a-input>
</a-form-item>
</a-col>
<a-col :md="6" :sm="8">
<span style="float: left;overflow: hidden;" class="table-page-search-submitButtons">
<a-button type="primary" @click="searchQuery" icon="search">查询</a-button>
<a-button type="primary" @click="searchReset" icon="reload" style="margin-left: 8px">重置</a-button>
</span>
</a-col>
</a-row>
</a-form>
</div>
<!-- 查询区域-END -->
<!-- table区域-begin -->
<div>
<div class="ant-alert ant-alert-info" style="margin-bottom: 16px;">
<i class="anticon anticon-info-circle ant-alert-icon"></i> 已选择 <a style="font-weight: 600">{{ selectedRowKeys.length }}</a>项
<a style="margin-left: 24px" @click="onClearSelected">清空</a>
</div>
<a-table
ref="table"
size="middle"
:scroll="{x:true}"
bordered
rowKey="token"
:columns="columns"
:dataSource="dataSource"
:pagination="ipagination"
:loading="loading"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
class="j-table-force-nowrap"
@change="handleTableChange">
<template slot="avatarslot" slot-scope="text, record, index">
<div class="anty-img-wrap">
<a-avatar shape="square" :src="getAvatarView(record.avatar)" icon="user"/>
</div>
</template>
<span slot="action" slot-scope="text, record">
<a-popconfirm title="强制退出用户?" @confirm="() => handleForce(record)">
<a-button type="danger">强退</a-button>
</a-popconfirm>
</span>
</a-table>
</div>
</a-card>
</template>
<script>
import '@/assets/less/TableExpand.less'
import { mixinDevice } from '@/utils/mixin'
import { JeecgListMixin } from '@/mixins/JeecgListMixin'
import { forceLogout } from '@/api/login'
import {filterDictTextByCache} from '@/components/dict/JDictSelectUtil'
import {getFileAccessHttpUrl} from '@/api/manage';
export default {
name: "OnlineList",
mixins:[JeecgListMixin, mixinDevice],
components: {},
data () {
return {
description: '在线用户管理页面',
queryParam: {
username: ''
},
// 表头
columns: [
{
title:'用户账号',
align:"center",
dataIndex: 'username'
},{
title:'用户姓名',
align:"center",
dataIndex: 'realname'
},{
title: '头像',
align: "center",
width: 120,
dataIndex: 'avatar',
scopedSlots: {customRender: "avatarslot"}
},{
title:'生日',
align:"center",
dataIndex: 'birthday'
},{
title: '性别',
align: "center",
dataIndex: 'sex',
customRender: (text) => {
//字典值翻译通用方法
return filterDictTextByCache('sex', text);
}
},{
title:'手机号',
align:"center",
dataIndex: 'phone'
},{
title: '操作',
dataIndex: 'action',
scopedSlots: {customRender: 'action'},
align: "center",
width: 170
}
],
url: {
list: "/sys/online/list"
},
dictOptions:{},
}
},
created() {
},
computed: {
importExcelUrl: function(){
return `${window._CONFIG['domianURL']}/${this.url.importExcelUrl}`;
},
},
methods: {
getAvatarView: function (avatar) {
return getFileAccessHttpUrl(avatar)
},
handleForce(record) {
let that = this;
let forceParam = {
token: record.token
}
return forceLogout(forceParam).then((res) => {
if (res.success) {
that.loadData();
this.$message.success('强制退出用户'+record.realname+'成功');
} else {
that.$message.warning(res.message);
}
})
}
}
}
</script>
<style scoped>
@import '~@assets/less/common.less';
</style>

View File

@ -5687,6 +5687,7 @@ INSERT INTO `sys_permission` VALUES ('fb367426764077dcf94640c843733985', '2a470f
INSERT INTO `sys_permission` VALUES ('fba41089766888023411a978d13c0aa4', 'e41b69c57a941a3bbcce45032fe57605', 'AUTO树表单列表', '/online/cgformTreeList/:code', 'modules/online/cgform/auto/OnlCgformTreeList', NULL, NULL, 1, NULL, '1', 9.00, 0, NULL, 1, 1, NULL, 1, NULL, 'admin', '2019-05-21 14:46:50', 'admin', '2019-06-11 13:52:52', 0, 0, '1', NULL);
INSERT INTO `sys_permission` VALUES ('fc810a2267dd183e4ef7c71cc60f4670', '700b7f95165c46cc7a78bf227aa8fed3', '请求追踪', '/monitor/HttpTrace', 'modules/monitor/HttpTrace', NULL, NULL, 1, NULL, NULL, 4.00, 0, NULL, 1, 1, NULL, 0, NULL, 'admin', '2019-04-02 09:46:19', 'admin', '2019-04-02 11:37:27', 0, 0, NULL, NULL);
INSERT INTO `sys_permission` VALUES ('fedfbf4420536cacc0218557d263dfea', '6e73eb3c26099c191bf03852ee1310a1', '新消息通知', '/account/settings/notification', 'account/settings/Notification', NULL, NULL, 1, 'NotificationSettings', NULL, NULL, NULL, '', 1, 1, NULL, NULL, NULL, NULL, '2018-12-26 19:02:05', NULL, NULL, 0, 0, NULL, NULL);
INSERT INTO `sys_permission` VALUES ('1402436404646010882', '08e6b9dc3c04489c8e1ff2ce6f105aa4', '在线用户', '/isystem/online', 'system/SysOnlineList', NULL, NULL, 1, NULL, '1', 1.00, 0, NULL, 1, 1, 0, 0, NULL, 'admin', '2021-06-09 09:24:30', 'admin', '2021-06-09 09:37:20', 0, 0, '1', 0);
-- ----------------------------
-- Table structure for sys_permission_data_rule

View File

@ -13,6 +13,8 @@ ADD COLUMN `third_user_id` varchar(100) NULL COMMENT '第三方app用户账号'
-- 新增第三方APP消息测试菜单
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1387612436586065922', '2a470fc0c3954d9dbb61de6d80846549', '第三方APP消息测试', '/jeecg/ThirdAppMessageTest', 'jeecg/ThirdAppMessageTest', '1', NULL, NULL, '1', NULL, '1', '3', '0', NULL, '1', '0', '0', NULL, 'admin', '2021-04-29 11:39:20', 'admin', '2021-04-29 11:39:27', '0', '0', '1', '0');
-- 新增监控在线用户
INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1402436404646010882', '08e6b9dc3c04489c8e1ff2ce6f105aa4', '在线用户', '/isystem/online', 'system/SysOnlineList', NULL, NULL, 1, NULL, '1', 1.00, 0, NULL, 1, 1, 0, 0, NULL, 'admin', '2021-06-09 09:24:30', 'admin', '2021-06-09 09:37:20', 0, 0, '1', 0);
-- 定时任务一个类允许配置多个调度
-- 删除定时任务表唯一索引

View File

@ -0,0 +1,81 @@
package org.jeecg.config;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.*;
import java.io.IOException;
@Configuration
@AutoConfigureAfter(DruidDataSourceAutoConfigure.class)
public class DruidConfig {
/**
* 带有广告的common.js全路径druid-1.1.14
*/
private static final String FILE_PATH = "support/http/resources/js/common.js";
/**
* 原始脚本触发构建广告的语句
*/
private static final String ORIGIN_JS = "this.buildFooter();";
/**
* 替换后的脚本
*/
private static final String NEW_JS = "//this.buildFooter();";
/**
* 去除Druid监控页面的广告
*
* @param properties DruidStatProperties属性集合
* @return {@link org.springframework.boot.web.servlet.FilterRegistrationBean}
*/
@Bean
@ConditionalOnWebApplication
@ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true")
public FilterRegistrationBean<RemoveAdFilter> removeDruidAdFilter(
DruidStatProperties properties) throws IOException {
// 获取web监控页面的参数
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
// 提取common.js的配置路径
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
// 获取common.js
String text = Utils.readFromResource(FILE_PATH);
// 屏蔽 this.buildFooter(); 不构建广告
final String newJs = text.replace(ORIGIN_JS, NEW_JS);
FilterRegistrationBean<RemoveAdFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RemoveAdFilter(newJs));
registration.addUrlPatterns(commonJsPattern);
return registration;
}
/**
* 删除druid的广告过滤器
*
* @author BBF
*/
private class RemoveAdFilter implements Filter {
private final String newJs;
public RemoveAdFilter(String newJS) {
this.newJs = newJS;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(request, response);
// 重置缓冲区响应头不会被重置
response.resetBuffer();
response.getWriter().write(newJs);
}
}
}

View File

@ -0,0 +1,128 @@
package org.jeecg.modules.system.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CacheConstant;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.base.service.BaseCommonService;
import org.jeecg.modules.system.service.ISysUserService;
import org.jeecg.modules.system.vo.SysOnlineVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* @Description: 在线用户
* @Author: chenli
* @Date: 2020-06-07
* @Version: V1.0
*/
@RestController
@RequestMapping("/sys/online")
@Slf4j
public class SysOnlineController {
@Autowired
private RedisUtil redisUtil;
@Autowired
public RedisTemplate redisTemplate;
@Autowired
public ISysUserService userService;
@Autowired
private ISysBaseAPI sysBaseAPI;
@Resource
private BaseCommonService baseCommonService;
@RequestMapping(value = "/list", method = RequestMethod.GET)
public Result<Page<SysOnlineVO>> list(@RequestParam(name="username", required=false) String username, @RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize) {
Collection<String> keys = redisTemplate.keys(CommonConstant.PREFIX_USER_TOKEN + "*");
SysOnlineVO online;
List<SysOnlineVO> onlineList = new ArrayList<SysOnlineVO>();
for (String key : keys) {
online = new SysOnlineVO();
String token = (String) redisUtil.get(key);
if (!StringUtils.isEmpty(token)){
online.setToken(token);
LoginUser loginUser = sysBaseAPI.getUserByName(JwtUtil.getUsername(token));
BeanUtils.copyProperties(loginUser, online);
if (StringUtils.isNotEmpty(username)) {
if (StringUtils.equals(username, online.getUsername())) {
onlineList.add(online);
}
} else {
onlineList.add(online);
}
}
}
Page<SysOnlineVO> page = new Page<SysOnlineVO>(pageNo, pageSize);
int count = onlineList.size();
List<SysOnlineVO> pages = new ArrayList<>();
//计算当前页第一条数据的下标
int currId = pageNo>1 ? (pageNo-1)*pageSize:0;
for (int i=0; i<pageSize && i<count - currId;i++){
pages.add(onlineList.get(currId+i));
}
page.setSize(pageSize);
page.setCurrent(pageNo);
page.setTotal(count);
//计算分页总页数
page.setPages(count %10 == 0 ? count/10 :count/10+1);
page.setRecords(pages);
Collections.reverse(onlineList);
onlineList.removeAll(Collections.singleton(null));
Result<Page<SysOnlineVO>> result = new Result<Page<SysOnlineVO>>();
result.setSuccess(true);
result.setResult(page);
return result;
}
/**
* 强退用户
*/
@RequestMapping(value = "/forceLogout",method = RequestMethod.POST)
public Result<Object> forceLogout(@RequestBody SysOnlineVO online) {
//用户退出逻辑
if(oConvertUtils.isEmpty(online.getToken())) {
return Result.error("退出登录失败!");
}
String username = JwtUtil.getUsername(online.getToken());
LoginUser sysUser = sysBaseAPI.getUserByName(username);
if(sysUser!=null) {
baseCommonService.addLog("强制: "+sysUser.getRealname()+"退出成功!", CommonConstant.LOG_TYPE_1, null,sysUser);
log.info(" 强制 "+sysUser.getRealname()+"退出成功! ");
//清空用户登录Token缓存
redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + online.getToken());
//清空用户登录Shiro权限缓存
redisUtil.del(CommonConstant.PREFIX_USER_SHIRO_CACHE + sysUser.getId());
//清空用户的缓存信息包括部门信息例如sys:cache:user::<username>
redisUtil.del(String.format("%s::%s", CacheConstant.SYS_USERS_CACHE, sysUser.getUsername()));
//调用shiro的logout
SecurityUtils.getSubject().logout();
return Result.ok("退出登录成功!");
}else {
return Result.error("Token无效!");
}
}
}

View File

@ -0,0 +1,60 @@
package org.jeecg.modules.system.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.jeecg.common.aspect.annotation.Dict;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
*
* @Author: chenli
* @Date: 2020-06-07
* @Version: V1.0
*/
@Data
public class SysOnlineVO {
/**
* 会话id
*/
private String id;
/**
* 会话编号
*/
private String token;
/**
* 用户名
*/
private String username;
/**
* 用户名
*/
private String realname;
/**
* 头像
*/
private String avatar;
/**
* 生日
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
/**
* 性别1 2
*/
@Dict(dicCode = "sex")
private Integer sex;
/**
* 手机号
*/
private String phone;
}