From 5889fc8facde4774e54ddbf34df14b812cbd2436 Mon Sep 17 00:00:00 2001 From: 二世阿博 <413940119@qq.com> Date: Fri, 11 Jun 2021 20:03:21 +0800 Subject: [PATCH] !62 新增监控在线用户 * 去掉durid广告 * 新增监控在线用户功能 * 新增监控在线用户功能 --- ant-design-vue-jeecg/src/api/login.js | 13 +++++++++++++ ant-design-vue-jeecg/src/views/system/SysOnlineList.vue | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jeecg-boot/db/jeecgboot-mysql-5.7.sql | 1 + jeecg-boot/db/增量SQL/2.4.3升级到2.4.5增量MYSQL.sql | 2 ++ jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/config/DruidConfig.java | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/controller/SysOnlineController.java | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/vo/SysOnlineVO.java | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 443 insertions(+), 0 deletions(-) create mode 100644 ant-design-vue-jeecg/src/views/system/SysOnlineList.vue create mode 100644 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/config/DruidConfig.java create mode 100644 jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/controller/SysOnlineController.java create mode 100644 jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/vo/SysOnlineVO.java diff --git a/ant-design-vue-jeecg/src/api/login.js b/ant-design-vue-jeecg/src/api/login.js index b359989..7bd6344 100644 --- a/ant-design-vue-jeecg/src/api/login.js +++ b/ant-design-vue-jeecg/src/api/login.js @@ -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 + }) } \ No newline at end of file diff --git a/ant-design-vue-jeecg/src/views/system/SysOnlineList.vue b/ant-design-vue-jeecg/src/views/system/SysOnlineList.vue new file mode 100644 index 0000000..3858c53 --- /dev/null +++ b/ant-design-vue-jeecg/src/views/system/SysOnlineList.vue @@ -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> \ No newline at end of file diff --git a/jeecg-boot/db/jeecgboot-mysql-5.7.sql b/jeecg-boot/db/jeecgboot-mysql-5.7.sql index b6576ef..0b46c45 100644 --- a/jeecg-boot/db/jeecgboot-mysql-5.7.sql +++ b/jeecg-boot/db/jeecgboot-mysql-5.7.sql @@ -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 diff --git a/jeecg-boot/db/增量SQL/2.4.3升级到2.4.5增量MYSQL.sql b/jeecg-boot/db/增量SQL/2.4.3升级到2.4.5增量MYSQL.sql index 16aabcb..0bc7818 100644 --- a/jeecg-boot/db/增量SQL/2.4.3升级到2.4.5增量MYSQL.sql +++ b/jeecg-boot/db/增量SQL/2.4.3升级到2.4.5增量MYSQL.sql @@ -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); -- 定时任务:一个类允许配置多个调度 -- 删除定时任务表唯一索引 diff --git a/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/config/DruidConfig.java b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/config/DruidConfig.java new file mode 100644 index 0000000..c8a2ed6 --- /dev/null +++ b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/config/DruidConfig.java @@ -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); + } + } +} diff --git a/jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/controller/SysOnlineController.java b/jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/controller/SysOnlineController.java new file mode 100644 index 0000000..8c7c1f2 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/controller/SysOnlineController.java @@ -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无效!"); + } + } +} diff --git a/jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/vo/SysOnlineVO.java b/jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/vo/SysOnlineVO.java new file mode 100644 index 0000000..a1c236f --- /dev/null +++ b/jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/vo/SysOnlineVO.java @@ -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; +} -- libgit2 0.22.2