mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 00:57:13 +08:00 
			
		
		
		
	新增:新增系统监控/在线用户功能,并优化部分注释规范
This commit is contained in:
		| @@ -251,10 +251,11 @@ continew-admin | ||||
|     │  ├─ views             # 页面模板 | ||||
|     │  │  ├─ login            # 登录模块 | ||||
|     │  │  ├─ monitor          # 系统监控模块 | ||||
|     │  │  │ └─ log              # 日志管理 | ||||
|     │  │  │   ├─ login            # 登录日志 | ||||
|     │  │  │   ├─ operation        # 操作日志 | ||||
|     │  │  │   └─ system           # 系统日志 | ||||
|     │  │  │  ├─ log              # 日志管理 | ||||
|     │  │  │  │  ├─ login            # 登录日志 | ||||
|     │  │  │  │  ├─ operation        # 操作日志 | ||||
|     │  │  │  │  └─ system           # 系统日志 | ||||
|     │  │  │  └─ online           # 在线用户 | ||||
|     │  │  └─ system           # 系统管理模块 | ||||
|     │  │    └─ user             # 用户模块 | ||||
|     │  │      └─ center           # 个人中心 | ||||
|   | ||||
| @@ -169,7 +169,20 @@ public class GlobalExceptionHandler { | ||||
|     @ExceptionHandler(NotLoginException.class) | ||||
|     public R handleNotLoginException(NotLoginException e, HttpServletRequest request) { | ||||
|         log.error("请求地址'{}',认证失败,无法访问系统资源", request.getRequestURI(), e); | ||||
|         String errorMsg = "登录状态已过期,请重新登录"; | ||||
|  | ||||
|         String errorMsg; | ||||
|         switch (e.getType()) { | ||||
|             case NotLoginException.KICK_OUT: | ||||
|                 errorMsg = "您已被踢下线"; | ||||
|                 break; | ||||
|             case NotLoginException.BE_REPLACED_MESSAGE: | ||||
|                 errorMsg = "您已被顶下线"; | ||||
|                 break; | ||||
|             default: | ||||
|                 errorMsg = "登录状态已过期,请重新登录"; | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         LogContextHolder.setErrorMsg(errorMsg); | ||||
|         return R.fail(HttpStatus.UNAUTHORIZED.value(), errorMsg); | ||||
|     } | ||||
|   | ||||
| @@ -83,4 +83,29 @@ public class LoginUser implements Serializable { | ||||
|      * 创建时间 | ||||
|      */ | ||||
|     private LocalDateTime createTime; | ||||
|  | ||||
|     /** | ||||
|      * 令牌 | ||||
|      */ | ||||
|     private String token; | ||||
|  | ||||
|     /** | ||||
|      * 登录 IP | ||||
|      */ | ||||
|     private String clientIp; | ||||
|  | ||||
|     /** | ||||
|      * 登录地点 | ||||
|      */ | ||||
|     private String location; | ||||
|  | ||||
|     /** | ||||
|      * 浏览器 | ||||
|      */ | ||||
|     private String browser; | ||||
|  | ||||
|     /** | ||||
|      * 登录时间 | ||||
|      */ | ||||
|     private LocalDateTime loginTime; | ||||
| } | ||||
|   | ||||
| @@ -78,6 +78,10 @@ public class PageQuery implements Serializable { | ||||
|         this.size = DEFAULT_SIZE; | ||||
|     } | ||||
|  | ||||
|     public int getPage() { | ||||
|         return page < 0 ? DEFAULT_PAGE : page; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 解析排序条件为 Spring 分页排序实体 | ||||
|      * | ||||
|   | ||||
| @@ -16,6 +16,7 @@ | ||||
|  | ||||
| package top.charles7c.cnadmin.common.model.vo; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import lombok.Data; | ||||
| @@ -26,6 +27,7 @@ import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import com.baomidou.mybatisplus.core.metadata.IPage; | ||||
|  | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
|  | ||||
| /** | ||||
|  * 分页信息 | ||||
| @@ -93,4 +95,37 @@ public class PageInfo<V> { | ||||
|         pageInfo.setTotal(pageInfo.getTotal()); | ||||
|         return pageInfo; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 基于列表数据构建分页信息 | ||||
|      * | ||||
|      * @param page | ||||
|      *            页码 | ||||
|      * @param size | ||||
|      *            每页记录数 | ||||
|      * @param list | ||||
|      *            列表数据 | ||||
|      * @param <V> | ||||
|      *            列表数据类型 | ||||
|      * @return 分页信息 | ||||
|      */ | ||||
|     public static <V> PageInfo<V> build(int page, int size, List<V> list) { | ||||
|         PageInfo<V> pageInfo = new PageInfo<>(); | ||||
|         if (CollUtil.isEmpty(list)) { | ||||
|             return pageInfo; | ||||
|         } | ||||
|  | ||||
|         pageInfo.setTotal(list.size()); | ||||
|         // 对列表数据进行分页 | ||||
|         int fromIndex = (page - 1) * size; | ||||
|         int toIndex = page * size + size; | ||||
|         if (fromIndex > list.size()) { | ||||
|             pageInfo.setList(new ArrayList<>()); | ||||
|         } else if (toIndex >= list.size()) { | ||||
|             pageInfo.setList(list.subList(fromIndex, list.size())); | ||||
|         } else { | ||||
|             pageInfo.setList(list.subList(fromIndex, toIndex)); | ||||
|         } | ||||
|         return pageInfo; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -16,15 +16,24 @@ | ||||
|  | ||||
| package top.charles7c.cnadmin.common.util.helper; | ||||
|  | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
|  | ||||
| import lombok.AccessLevel; | ||||
| import lombok.NoArgsConstructor; | ||||
|  | ||||
| import cn.dev33.satoken.context.SaHolder; | ||||
| import cn.dev33.satoken.stp.StpUtil; | ||||
| import cn.hutool.extra.servlet.ServletUtil; | ||||
|  | ||||
| import top.charles7c.cnadmin.common.consts.CacheConstants; | ||||
| import top.charles7c.cnadmin.common.model.dto.LogContext; | ||||
| import top.charles7c.cnadmin.common.model.dto.LoginUser; | ||||
| import top.charles7c.cnadmin.common.util.ExceptionUtils; | ||||
| import top.charles7c.cnadmin.common.util.IpUtils; | ||||
| import top.charles7c.cnadmin.common.util.ServletUtils; | ||||
| import top.charles7c.cnadmin.common.util.holder.LogContextHolder; | ||||
|  | ||||
| /** | ||||
|  * 登录助手 | ||||
| @@ -42,8 +51,22 @@ public class LoginHelper { | ||||
|      *            登录用户信息 | ||||
|      */ | ||||
|     public static void login(LoginUser loginUser) { | ||||
|         SaHolder.getStorage().set(CacheConstants.LOGIN_USER_CACHE_KEY, loginUser); | ||||
|         if (loginUser == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // 记录登录信息 | ||||
|         HttpServletRequest request = ServletUtils.getRequest(); | ||||
|         loginUser.setClientIp(ServletUtil.getClientIP(request)); | ||||
|         loginUser.setLocation(IpUtils.getCityInfo(loginUser.getClientIp())); | ||||
|         loginUser.setBrowser(ServletUtils.getBrowser(request)); | ||||
|         LogContext logContext = LogContextHolder.get(); | ||||
|         loginUser.setLoginTime(logContext != null ? logContext.getCreateTime() : LocalDateTime.now()); | ||||
|  | ||||
|         // 登录保存用户信息 | ||||
|         StpUtil.login(loginUser.getUserId()); | ||||
|         loginUser.setToken(StpUtil.getTokenValue()); | ||||
|         SaHolder.getStorage().set(CacheConstants.LOGIN_USER_CACHE_KEY, loginUser); | ||||
|         StpUtil.getTokenSession().set(CacheConstants.LOGIN_USER_CACHE_KEY, loginUser); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -39,7 +39,7 @@ public class SysLog implements Serializable { | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 日志ID | ||||
|      * 日志 ID | ||||
|      */ | ||||
|     @TableId | ||||
|     private Long logId; | ||||
|   | ||||
| @@ -0,0 +1,52 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.charles7c.cnadmin.monitor.model.query; | ||||
|  | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
|  | ||||
| import org.springdoc.api.annotations.ParameterObject; | ||||
| import org.springframework.format.annotation.DateTimeFormat; | ||||
|  | ||||
| /** | ||||
|  * 在线用户查询条件 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/20 23:07 | ||||
|  */ | ||||
| @Data | ||||
| @ParameterObject | ||||
| @Schema(description = "在线用户查询条件") | ||||
| public class OnlineUserQuery { | ||||
|  | ||||
|     /** | ||||
|      * 用户昵称 | ||||
|      */ | ||||
|     @Schema(description = "用户昵称") | ||||
|     private String nickname; | ||||
|  | ||||
|     /** | ||||
|      * 登录时间 | ||||
|      */ | ||||
|     @Schema(description = "登录时间") | ||||
|     @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||||
|     private List<Date> loginTime; | ||||
| } | ||||
| @@ -38,9 +38,9 @@ public class LoginLogVO extends LogVO implements Serializable { | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 日志ID | ||||
|      * 日志 ID | ||||
|      */ | ||||
|     @Schema(description = "日志ID") | ||||
|     @Schema(description = "日志 ID") | ||||
|     private Long logId; | ||||
|  | ||||
|     /** | ||||
| @@ -56,9 +56,9 @@ public class LoginLogVO extends LogVO implements Serializable { | ||||
|     private LogStatusEnum status; | ||||
|  | ||||
|     /** | ||||
|      * 登录IP | ||||
|      * 登录 IP | ||||
|      */ | ||||
|     @Schema(description = "登录IP") | ||||
|     @Schema(description = "登录 IP") | ||||
|     private String clientIp; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -0,0 +1,79 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.charles7c.cnadmin.monitor.model.vo; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
|  | ||||
| /** | ||||
|  * 在线用户信息 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/20 21:54 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "在线用户信息") | ||||
| public class OnlineUserVO implements Serializable { | ||||
|  | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 令牌 | ||||
|      */ | ||||
|     @Schema(description = "令牌") | ||||
|     private String token; | ||||
|  | ||||
|     /** | ||||
|      * 用户名 | ||||
|      */ | ||||
|     @Schema(description = "用户名") | ||||
|     private String username; | ||||
|  | ||||
|     /** | ||||
|      * 昵称 | ||||
|      */ | ||||
|     @Schema(description = "昵称") | ||||
|     private String nickname; | ||||
|  | ||||
|     /** | ||||
|      * 登录 IP | ||||
|      */ | ||||
|     @Schema(description = "登录 IP") | ||||
|     private String clientIp; | ||||
|  | ||||
|     /** | ||||
|      * 登录地点 | ||||
|      */ | ||||
|     @Schema(description = "登录地点") | ||||
|     private String location; | ||||
|  | ||||
|     /** | ||||
|      * 浏览器 | ||||
|      */ | ||||
|     @Schema(description = "浏览器") | ||||
|     private String browser; | ||||
|  | ||||
|     /** | ||||
|      * 登录时间 | ||||
|      */ | ||||
|     @Schema(description = "登录时间") | ||||
|     private LocalDateTime loginTime; | ||||
| } | ||||
| @@ -38,9 +38,9 @@ public class OperationLogVO extends LogVO implements Serializable { | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 日志ID | ||||
|      * 日志 ID | ||||
|      */ | ||||
|     @Schema(description = "日志ID") | ||||
|     @Schema(description = "日志 ID") | ||||
|     private Long logId; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -36,9 +36,9 @@ public class SystemLogDetailVO extends LogVO implements Serializable { | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 日志ID | ||||
|      * 日志 ID | ||||
|      */ | ||||
|     @Schema(description = "日志ID") | ||||
|     @Schema(description = "日志 ID") | ||||
|     private Long logId; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -36,9 +36,9 @@ public class SystemLogVO extends LogVO implements Serializable { | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 日志ID | ||||
|      * 日志 ID | ||||
|      */ | ||||
|     @Schema(description = "日志ID") | ||||
|     @Schema(description = "日志 ID") | ||||
|     private Long logId; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -71,7 +71,7 @@ public interface LogService { | ||||
|      * 查看系统日志详情 | ||||
|      * | ||||
|      * @param logId | ||||
|      *            日志ID | ||||
|      *            日志 ID | ||||
|      * @return 系统日志详情 | ||||
|      */ | ||||
|     SystemLogDetailVO detail(Long logId); | ||||
|   | ||||
							
								
								
									
										35
									
								
								continew-admin-ui/src/api/monitor/online.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								continew-admin-ui/src/api/monitor/online.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import axios from 'axios'; | ||||
| import qs from 'query-string'; | ||||
|  | ||||
| export interface OnlineUserRecord { | ||||
|   token: string; | ||||
|   username: string; | ||||
|   nickname: string; | ||||
|   clientIp: string; | ||||
|   location: string; | ||||
|   browser: string; | ||||
|   loginTime: string; | ||||
| } | ||||
|  | ||||
| export interface OnlineUserParams extends Partial<OnlineUserRecord> { | ||||
|   page: number; | ||||
|   size: number; | ||||
|   sort: Array<string>; | ||||
| } | ||||
| export interface OnlineUserListRes { | ||||
|   list: OnlineUserRecord[]; | ||||
|   total: number; | ||||
| } | ||||
|  | ||||
| export function queryOnlineUserList(params: OnlineUserParams) { | ||||
|   return axios.get<OnlineUserListRes>('/monitor/online/user', { | ||||
|     params, | ||||
|     paramsSerializer: (obj) => { | ||||
|       return qs.stringify(obj); | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export function kickout(token: string) { | ||||
|   return axios.delete(`/monitor/online/user/${token}`); | ||||
| } | ||||
| @@ -8,6 +8,7 @@ import localeMonitor from '@/views/dashboard/monitor/locale/en-US'; | ||||
| import localeDataAnalysis from '@/views/visualization/data-analysis/locale/en-US'; | ||||
| import localeMultiDAnalysis from '@/views/visualization/multi-dimension-data-analysis/locale/en-US'; | ||||
|  | ||||
| import localeOnlineUser from '@/views/monitor/online/locale/en-US'; | ||||
| import localeLoginLog from '@/views/monitor/log/login/locale/en-US'; | ||||
| import localeOperationLog from '@/views/monitor/log/operation/locale/en-US'; | ||||
| import localeSystemLog from '@/views/monitor/log/system/locale/en-US'; | ||||
| @@ -57,6 +58,7 @@ export default { | ||||
|   ...localeDataAnalysis, | ||||
|   ...localeMultiDAnalysis, | ||||
|  | ||||
|   ...localeOnlineUser, | ||||
|   ...localeLoginLog, | ||||
|   ...localeOperationLog, | ||||
|   ...localeSystemLog, | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import localeMonitor from '@/views/dashboard/monitor/locale/zh-CN'; | ||||
| import localeDataAnalysis from '@/views/visualization/data-analysis/locale/zh-CN'; | ||||
| import localeMultiDAnalysis from '@/views/visualization/multi-dimension-data-analysis/locale/zh-CN'; | ||||
|  | ||||
| import localeOnlineUser from '@/views/monitor/online/locale/zh-CN'; | ||||
| import localeLoginLog from '@/views/monitor/log/login/locale/zh-CN'; | ||||
| import localeOperationLog from '@/views/monitor/log/operation/locale/zh-CN'; | ||||
| import localeSystemLog from '@/views/monitor/log/system/locale/zh-CN'; | ||||
| @@ -57,6 +58,7 @@ export default { | ||||
|   ...localeDataAnalysis, | ||||
|   ...localeMultiDAnalysis, | ||||
|  | ||||
|   ...localeOnlineUser, | ||||
|   ...localeLoginLog, | ||||
|   ...localeOperationLog, | ||||
|   ...localeSystemLog, | ||||
|   | ||||
| @@ -12,6 +12,16 @@ const Monitor: AppRouteRecordRaw = { | ||||
|     order: 2, | ||||
|   }, | ||||
|   children: [ | ||||
|     { | ||||
|       path: '/online', | ||||
|       name: 'OnlineUser', | ||||
|       component: () => import('@/views/monitor/online/index.vue'), | ||||
|       meta: { | ||||
|         locale: 'menu.online.user.list', | ||||
|         requiresAuth: true, | ||||
|         roles: ['*'], | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       path: 'log/login', | ||||
|       name: 'LoginLog', | ||||
|   | ||||
| @@ -4,15 +4,8 @@ | ||||
|     <a-card class="general-card" :title="$t('menu.log.login.list')"> | ||||
|       <a-row style="margin-bottom: 15px"> | ||||
|         <a-col :span="24"> | ||||
|           <a-form | ||||
|             ref="queryFormRef" | ||||
|             :model="queryFormData" | ||||
|             layout="inline" | ||||
|           > | ||||
|             <a-form-item | ||||
|               field="status" | ||||
|               hide-label | ||||
|             > | ||||
|           <a-form ref="queryFormRef" :model="queryFormData" layout="inline"> | ||||
|             <a-form-item field="status" hide-label> | ||||
|               <a-select | ||||
|                 v-model="queryFormData.status" | ||||
|                 :options="statusOptions" | ||||
| @@ -21,10 +14,7 @@ | ||||
|                 style="width: 150px;" | ||||
|               /> | ||||
|             </a-form-item> | ||||
|             <a-form-item | ||||
|               field="createTime" | ||||
|               hide-label | ||||
|             > | ||||
|             <a-form-item field="createTime" hide-label> | ||||
|               <date-range-picker v-model="queryFormData.createTime" /> | ||||
|             </a-form-item> | ||||
|             <a-button type="primary" @click="toQuery"> | ||||
|   | ||||
| @@ -4,15 +4,8 @@ | ||||
|     <a-card class="general-card" :title="$t('menu.log.operation.list')"> | ||||
|       <a-row style="margin-bottom: 15px"> | ||||
|         <a-col :span="24"> | ||||
|           <a-form | ||||
|             ref="queryFormRef" | ||||
|             :model="queryFormData" | ||||
|             layout="inline" | ||||
|           > | ||||
|             <a-form-item | ||||
|               field="description" | ||||
|               hide-label | ||||
|             > | ||||
|           <a-form ref="queryFormRef" :model="queryFormData" layout="inline"> | ||||
|             <a-form-item field="description" hide-label> | ||||
|               <a-input | ||||
|                 v-model="queryFormData.description" | ||||
|                 placeholder="输入操作内容搜索" | ||||
| @@ -21,10 +14,7 @@ | ||||
|                 @press-enter="toQuery" | ||||
|               /> | ||||
|             </a-form-item> | ||||
|             <a-form-item | ||||
|               field="status" | ||||
|               hide-label | ||||
|             > | ||||
|             <a-form-item field="status" hide-label> | ||||
|               <a-select | ||||
|                 v-model="queryFormData.status" | ||||
|                 :options="statusOptions" | ||||
| @@ -33,10 +23,7 @@ | ||||
|                 style="width: 150px;" | ||||
|               /> | ||||
|             </a-form-item> | ||||
|             <a-form-item | ||||
|               field="createTime" | ||||
|               hide-label | ||||
|             > | ||||
|             <a-form-item field="createTime" hide-label> | ||||
|               <date-range-picker v-model="queryFormData.createTime" /> | ||||
|             </a-form-item> | ||||
|             <a-button type="primary" @click="toQuery"> | ||||
|   | ||||
							
								
								
									
										202
									
								
								continew-admin-ui/src/views/monitor/online/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								continew-admin-ui/src/views/monitor/online/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | ||||
| <template> | ||||
|   <div class="container"> | ||||
|     <Breadcrumb :items="['menu.monitor', 'menu.online.user.list']" /> | ||||
|     <a-card class="general-card" :title="$t('menu.online.user.list')"> | ||||
|       <a-row style="margin-bottom: 15px"> | ||||
|         <a-col :span="24"> | ||||
|           <a-form ref="queryFormRef" :model="queryFormData" layout="inline"> | ||||
|             <a-form-item field="nickname" hide-label> | ||||
|               <a-input | ||||
|                 v-model="queryFormData.nickname" | ||||
|                 placeholder="输入用户昵称搜索" | ||||
|                 allow-clear | ||||
|                 style="width: 150px;" | ||||
|                 @press-enter="toQuery" | ||||
|               /> | ||||
|             </a-form-item> | ||||
|             <a-form-item field="loginTime" hide-label> | ||||
|               <date-range-picker v-model="queryFormData.loginTime" /> | ||||
|             </a-form-item> | ||||
|             <a-button type="primary" @click="toQuery"> | ||||
|               <template #icon> | ||||
|                 <icon-search /> | ||||
|               </template> | ||||
|               查询 | ||||
|             </a-button> | ||||
|             <a-button @click="resetQuery"> | ||||
|               <template #icon> | ||||
|                 <icon-refresh /> | ||||
|               </template> | ||||
|               重置 | ||||
|             </a-button> | ||||
|           </a-form> | ||||
|         </a-col> | ||||
|       </a-row> | ||||
|       <a-table | ||||
|         :columns="columns" | ||||
|         :data="renderData" | ||||
|         :pagination="paginationProps" | ||||
|         row-key="logId" | ||||
|         :bordered="false" | ||||
|         :stripe="true" | ||||
|         :loading="loading" | ||||
|         size="large" | ||||
|         @page-change="handlePageChange" | ||||
|         @page-size-change="handlePageSizeChange" | ||||
|       > | ||||
|         <template #index="{ rowIndex }"> | ||||
|           {{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }} | ||||
|         </template> | ||||
|         <template #nickname="{ record }"> | ||||
|           {{ record.nickname }}({{record.username}}) | ||||
|         </template> | ||||
|         <template #operations="{ record }"> | ||||
|           <a-button | ||||
|             v-permission="['admin']" | ||||
|             type="text" | ||||
|             size="small" | ||||
|             :title="currentToken === record.token ? '不能强退当前登录' : ''" | ||||
|             :disabled="currentToken === record.token" | ||||
|             @click="handleClick(record.token)" | ||||
|           > | ||||
|             强退 | ||||
|           </a-button> | ||||
|         </template> | ||||
|       </a-table> | ||||
|     </a-card> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
|   import { computed, ref, reactive } from 'vue'; | ||||
|   import useLoading from '@/hooks/loading'; | ||||
|   import { Message } from '@arco-design/web-vue'; | ||||
|   import { queryOnlineUserList, OnlineUserRecord, OnlineUserParams, kickout } from '@/api/monitor/online'; | ||||
|   import { Pagination } from '@/types/global'; | ||||
|   import { PaginationProps } from '@arco-design/web-vue'; | ||||
|   import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'; | ||||
|   import { FormInstance } from '@arco-design/web-vue/es/form'; | ||||
|   import { getToken } from '@/utils/auth'; | ||||
|  | ||||
|   const { loading, setLoading } = useLoading(true); | ||||
|   const currentToken = computed(() => getToken()); | ||||
|   const queryFormRef = ref<FormInstance>(); | ||||
|   const queryFormData = ref({ | ||||
|     nickname: '', | ||||
|     status: undefined, | ||||
|     loginTime: [], | ||||
|   }); | ||||
|  | ||||
|   // 查询 | ||||
|   const toQuery = () => { | ||||
|     fetchData({ | ||||
|       page: pagination.current, | ||||
|       size: pagination.pageSize, | ||||
|       sort: ['createTime,desc'], | ||||
|       ...queryFormData.value, | ||||
|     } as unknown as OnlineUserParams); | ||||
|   }; | ||||
|  | ||||
|   // 重置 | ||||
|   const resetQuery = async () => { | ||||
|     await queryFormRef.value?.resetFields(); | ||||
|     await fetchData(); | ||||
|   }; | ||||
|  | ||||
|   const renderData = ref<OnlineUserRecord[]>([]); | ||||
|   const basePagination: Pagination = { | ||||
|     current: 1, | ||||
|     pageSize: 10, | ||||
|   }; | ||||
|   const pagination = reactive({ | ||||
|     ...basePagination, | ||||
|   }); | ||||
|   const paginationProps = computed((): PaginationProps => { | ||||
|     return { | ||||
|       showTotal: true, | ||||
|       showPageSize: true, | ||||
|       total: pagination.total, | ||||
|       current: pagination.current, | ||||
|     } | ||||
|   }); | ||||
|   const columns = computed<TableColumnData[]>(() => [ | ||||
|     { | ||||
|       title: '序号', | ||||
|       dataIndex: 'index', | ||||
|       slotName: 'index', | ||||
|     }, | ||||
|     { | ||||
|       title: '用户昵称', | ||||
|       dataIndex: 'nickname', | ||||
|       slotName: 'nickname', | ||||
|     }, | ||||
|     { | ||||
|       title: '登录 IP', | ||||
|       dataIndex: 'clientIp', | ||||
|     }, | ||||
|     { | ||||
|       title: '登录地点', | ||||
|       dataIndex: 'location', | ||||
|     }, | ||||
|     { | ||||
|       title: '浏览器', | ||||
|       dataIndex: 'browser', | ||||
|     }, | ||||
|     { | ||||
|       title: '登录时间', | ||||
|       dataIndex: 'loginTime', | ||||
|     }, | ||||
|     { | ||||
|       title: '操作', | ||||
|       slotName: 'operations', | ||||
|       align: 'center', | ||||
|     }, | ||||
|   ]); | ||||
|  | ||||
|   // 分页查询列表 | ||||
|   const fetchData = async ( | ||||
|     params: OnlineUserParams = { page: 1, size: 10, sort: ['createTime,desc'] } | ||||
|   ) => { | ||||
|     setLoading(true); | ||||
|     try { | ||||
|       const { data } = await queryOnlineUserList(params); | ||||
|       renderData.value = data.list; | ||||
|       pagination.current = params.page; | ||||
|       pagination.total = data.total; | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|   const handlePageChange = (current: number) => { | ||||
|     fetchData({ page: current, size: pagination.pageSize, sort: ['createTime,desc'] }); | ||||
|   }; | ||||
|   const handlePageSizeChange = (pageSize: number) => { | ||||
|     fetchData({ page: pagination.current, size: pageSize, sort: ['createTime,desc'] }); | ||||
|   }; | ||||
|   fetchData(); | ||||
|  | ||||
|   // 强退 | ||||
|   const handleClick = async (token: string) => { | ||||
|     const res = await kickout(token); | ||||
|     if (res.success) Message.success(res.msg); | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
|   export default { | ||||
|     name: 'OnlineUser', | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="less"> | ||||
|   .container { | ||||
|     padding: 0 20px 20px 20px; | ||||
|   } | ||||
|   :deep(.arco-table-th) { | ||||
|     &:last-child { | ||||
|       .arco-table-th-item-title { | ||||
|         margin-left: 16px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| </style> | ||||
| @@ -0,0 +1,3 @@ | ||||
| export default { | ||||
|   'menu.online.user.list': 'Online user', | ||||
| }; | ||||
| @@ -0,0 +1,3 @@ | ||||
| export default { | ||||
|   'menu.online.user.list': '在线用户', | ||||
| }; | ||||
| @@ -0,0 +1,126 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.charles7c.cnadmin.webapi.controller.monitor; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Comparator; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
|  | ||||
| import lombok.RequiredArgsConstructor; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
|  | ||||
| import org.springframework.http.MediaType; | ||||
| import org.springframework.validation.annotation.Validated; | ||||
| import org.springframework.web.bind.annotation.*; | ||||
|  | ||||
| import cn.dev33.satoken.dao.SaTokenDao; | ||||
| import cn.dev33.satoken.session.SaSession; | ||||
| import cn.dev33.satoken.stp.StpUtil; | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.date.DateUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
|  | ||||
| import top.charles7c.cnadmin.common.consts.CacheConstants; | ||||
| import top.charles7c.cnadmin.common.model.dto.LoginUser; | ||||
| import top.charles7c.cnadmin.common.model.query.PageQuery; | ||||
| import top.charles7c.cnadmin.common.model.vo.PageInfo; | ||||
| import top.charles7c.cnadmin.common.model.vo.R; | ||||
| import top.charles7c.cnadmin.common.util.validate.ValidationUtils; | ||||
| import top.charles7c.cnadmin.monitor.model.query.OnlineUserQuery; | ||||
| import top.charles7c.cnadmin.monitor.model.vo.*; | ||||
|  | ||||
| /** | ||||
|  * 在线用户 API | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/20 21:51 | ||||
|  */ | ||||
| @Tag(name = "在线用户 API") | ||||
| @Validated | ||||
| @RestController | ||||
| @RequiredArgsConstructor | ||||
| @RequestMapping(value = "/monitor/online/user", produces = MediaType.APPLICATION_JSON_VALUE) | ||||
| public class OnlineUserController { | ||||
|  | ||||
|     @Operation(summary = "分页查询在线用户列表") | ||||
|     @GetMapping | ||||
|     public R<PageInfo<OnlineUserVO>> list(@Validated OnlineUserQuery query, @Validated PageQuery pageQuery) { | ||||
|         List<LoginUser> loginUserList = new ArrayList<>(); | ||||
|         List<String> tokenKeyList = StpUtil.searchTokenValue("", 0, -1, false); | ||||
|         for (String tokenKey : tokenKeyList) { | ||||
|             String token = StrUtil.subAfter(tokenKey, ":", true); | ||||
|             // 忽略已过期或失效 token | ||||
|             if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < SaTokenDao.NEVER_EXPIRE) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             // 获取 Token Session | ||||
|             SaSession saSession = StpUtil.getTokenSessionByToken(token); | ||||
|             LoginUser loginUser = saSession.get(CacheConstants.LOGIN_USER_CACHE_KEY, new LoginUser()); | ||||
|  | ||||
|             // 检查是否符合查询条件 | ||||
|             if (Boolean.TRUE.equals(checkQuery(query, loginUser))) { | ||||
|                 loginUserList.add(loginUser); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 构建分页数据 | ||||
|         List<OnlineUserVO> onlineUserList = BeanUtil.copyToList(loginUserList, OnlineUserVO.class); | ||||
|         CollUtil.sort(onlineUserList, Comparator.comparing(OnlineUserVO::getLoginTime).reversed()); | ||||
|         PageInfo<OnlineUserVO> pageInfo = PageInfo.build(pageQuery.getPage(), pageQuery.getSize(), onlineUserList); | ||||
|         return R.ok(pageInfo); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检查是否符合查询条件 | ||||
|      * | ||||
|      * @param query | ||||
|      *            查询条件 | ||||
|      * @param loginUser | ||||
|      *            登录用户信息 | ||||
|      * @return 是否符合查询条件 | ||||
|      */ | ||||
|     private boolean checkQuery(OnlineUserQuery query, LoginUser loginUser) { | ||||
|         boolean flag1 = true; | ||||
|         String nickname = query.getNickname(); | ||||
|         if (StrUtil.isNotBlank(nickname)) { | ||||
|             flag1 = loginUser.getUsername().contains(nickname) || loginUser.getNickname().contains(nickname); | ||||
|         } | ||||
|  | ||||
|         boolean flag2 = true; | ||||
|         List<Date> loginTime = query.getLoginTime(); | ||||
|         if (CollUtil.isNotEmpty(loginTime)) { | ||||
|             flag2 = | ||||
|                 DateUtil.isIn(DateUtil.date(loginUser.getLoginTime()).toJdkDate(), loginTime.get(0), loginTime.get(1)); | ||||
|         } | ||||
|         return flag1 && flag2; | ||||
|     } | ||||
|  | ||||
|     @Operation(summary = "强退在线用户") | ||||
|     @DeleteMapping("/{token}") | ||||
|     public R kickout(@PathVariable String token) { | ||||
|         String currentToken = StpUtil.getTokenValue(); | ||||
|         ValidationUtils.throwIfEqual(token, currentToken, "不能强退当前登录"); | ||||
|  | ||||
|         StpUtil.kickoutByTokenValue(token); | ||||
|         return R.ok("强退成功"); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user