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