mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 10:57:13 +08:00 
			
		
		
		
	新增:新增系统监控/在线用户功能,并优化部分注释规范
This commit is contained in:
		| @@ -251,10 +251,11 @@ continew-admin | |||||||
|     │  ├─ views             # 页面模板 |     │  ├─ views             # 页面模板 | ||||||
|     │  │  ├─ login            # 登录模块 |     │  │  ├─ login            # 登录模块 | ||||||
|     │  │  ├─ monitor          # 系统监控模块 |     │  │  ├─ monitor          # 系统监控模块 | ||||||
|     │  │  │ └─ log              # 日志管理 |     │  │  │  ├─ log              # 日志管理 | ||||||
|     │  │  │   ├─ login            # 登录日志 |     │  │  │  │  ├─ login            # 登录日志 | ||||||
|     │  │  │   ├─ operation        # 操作日志 |     │  │  │  │  ├─ operation        # 操作日志 | ||||||
|     │  │  │   └─ system           # 系统日志 |     │  │  │  │  └─ system           # 系统日志 | ||||||
|  |     │  │  │  └─ online           # 在线用户 | ||||||
|     │  │  └─ system           # 系统管理模块 |     │  │  └─ system           # 系统管理模块 | ||||||
|     │  │    └─ user             # 用户模块 |     │  │    └─ user             # 用户模块 | ||||||
|     │  │      └─ center           # 个人中心 |     │  │      └─ center           # 个人中心 | ||||||
|   | |||||||
| @@ -169,7 +169,20 @@ public class GlobalExceptionHandler { | |||||||
|     @ExceptionHandler(NotLoginException.class) |     @ExceptionHandler(NotLoginException.class) | ||||||
|     public R handleNotLoginException(NotLoginException e, HttpServletRequest request) { |     public R handleNotLoginException(NotLoginException e, HttpServletRequest request) { | ||||||
|         log.error("请求地址'{}',认证失败,无法访问系统资源", request.getRequestURI(), e); |         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); |         LogContextHolder.setErrorMsg(errorMsg); | ||||||
|         return R.fail(HttpStatus.UNAUTHORIZED.value(), errorMsg); |         return R.fail(HttpStatus.UNAUTHORIZED.value(), errorMsg); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -83,4 +83,29 @@ public class LoginUser implements Serializable { | |||||||
|      * 创建时间 |      * 创建时间 | ||||||
|      */ |      */ | ||||||
|     private LocalDateTime createTime; |     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; |         this.size = DEFAULT_SIZE; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public int getPage() { | ||||||
|  |         return page < 0 ? DEFAULT_PAGE : page; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 解析排序条件为 Spring 分页排序实体 |      * 解析排序条件为 Spring 分页排序实体 | ||||||
|      * |      * | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ | |||||||
|  |  | ||||||
| package top.charles7c.cnadmin.common.model.vo; | package top.charles7c.cnadmin.common.model.vo; | ||||||
|  |  | ||||||
|  | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
| import lombok.Data; | import lombok.Data; | ||||||
| @@ -26,6 +27,7 @@ import io.swagger.v3.oas.annotations.media.Schema; | |||||||
| import com.baomidou.mybatisplus.core.metadata.IPage; | import com.baomidou.mybatisplus.core.metadata.IPage; | ||||||
|  |  | ||||||
| import cn.hutool.core.bean.BeanUtil; | import cn.hutool.core.bean.BeanUtil; | ||||||
|  | import cn.hutool.core.collection.CollUtil; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 分页信息 |  * 分页信息 | ||||||
| @@ -93,4 +95,37 @@ public class PageInfo<V> { | |||||||
|         pageInfo.setTotal(pageInfo.getTotal()); |         pageInfo.setTotal(pageInfo.getTotal()); | ||||||
|         return pageInfo; |         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; | package top.charles7c.cnadmin.common.util.helper; | ||||||
|  |  | ||||||
|  | import java.time.LocalDateTime; | ||||||
|  |  | ||||||
|  | import javax.servlet.http.HttpServletRequest; | ||||||
|  |  | ||||||
| import lombok.AccessLevel; | import lombok.AccessLevel; | ||||||
| import lombok.NoArgsConstructor; | import lombok.NoArgsConstructor; | ||||||
|  |  | ||||||
| import cn.dev33.satoken.context.SaHolder; | import cn.dev33.satoken.context.SaHolder; | ||||||
| import cn.dev33.satoken.stp.StpUtil; | import cn.dev33.satoken.stp.StpUtil; | ||||||
|  | import cn.hutool.extra.servlet.ServletUtil; | ||||||
|  |  | ||||||
| import top.charles7c.cnadmin.common.consts.CacheConstants; | 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.model.dto.LoginUser; | ||||||
| import top.charles7c.cnadmin.common.util.ExceptionUtils; | 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) { |     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()); |         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); |         StpUtil.getTokenSession().set(CacheConstants.LOGIN_USER_CACHE_KEY, loginUser); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ public class SysLog implements Serializable { | |||||||
|     private static final long serialVersionUID = 1L; |     private static final long serialVersionUID = 1L; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 日志ID |      * 日志 ID | ||||||
|      */ |      */ | ||||||
|     @TableId |     @TableId | ||||||
|     private Long logId; |     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; |     private static final long serialVersionUID = 1L; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 日志ID |      * 日志 ID | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "日志ID") |     @Schema(description = "日志 ID") | ||||||
|     private Long logId; |     private Long logId; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -56,9 +56,9 @@ public class LoginLogVO extends LogVO implements Serializable { | |||||||
|     private LogStatusEnum status; |     private LogStatusEnum status; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 登录IP |      * 登录 IP | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "登录IP") |     @Schema(description = "登录 IP") | ||||||
|     private String clientIp; |     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; |     private static final long serialVersionUID = 1L; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 日志ID |      * 日志 ID | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "日志ID") |     @Schema(description = "日志 ID") | ||||||
|     private Long logId; |     private Long logId; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -36,9 +36,9 @@ public class SystemLogDetailVO extends LogVO implements Serializable { | |||||||
|     private static final long serialVersionUID = 1L; |     private static final long serialVersionUID = 1L; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 日志ID |      * 日志 ID | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "日志ID") |     @Schema(description = "日志 ID") | ||||||
|     private Long logId; |     private Long logId; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -36,9 +36,9 @@ public class SystemLogVO extends LogVO implements Serializable { | |||||||
|     private static final long serialVersionUID = 1L; |     private static final long serialVersionUID = 1L; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 日志ID |      * 日志 ID | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "日志ID") |     @Schema(description = "日志 ID") | ||||||
|     private Long logId; |     private Long logId; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -71,7 +71,7 @@ public interface LogService { | |||||||
|      * 查看系统日志详情 |      * 查看系统日志详情 | ||||||
|      * |      * | ||||||
|      * @param logId |      * @param logId | ||||||
|      *            日志ID |      *            日志 ID | ||||||
|      * @return 系统日志详情 |      * @return 系统日志详情 | ||||||
|      */ |      */ | ||||||
|     SystemLogDetailVO detail(Long logId); |     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 localeDataAnalysis from '@/views/visualization/data-analysis/locale/en-US'; | ||||||
| import localeMultiDAnalysis from '@/views/visualization/multi-dimension-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 localeLoginLog from '@/views/monitor/log/login/locale/en-US'; | ||||||
| import localeOperationLog from '@/views/monitor/log/operation/locale/en-US'; | import localeOperationLog from '@/views/monitor/log/operation/locale/en-US'; | ||||||
| import localeSystemLog from '@/views/monitor/log/system/locale/en-US'; | import localeSystemLog from '@/views/monitor/log/system/locale/en-US'; | ||||||
| @@ -57,6 +58,7 @@ export default { | |||||||
|   ...localeDataAnalysis, |   ...localeDataAnalysis, | ||||||
|   ...localeMultiDAnalysis, |   ...localeMultiDAnalysis, | ||||||
|  |  | ||||||
|  |   ...localeOnlineUser, | ||||||
|   ...localeLoginLog, |   ...localeLoginLog, | ||||||
|   ...localeOperationLog, |   ...localeOperationLog, | ||||||
|   ...localeSystemLog, |   ...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 localeDataAnalysis from '@/views/visualization/data-analysis/locale/zh-CN'; | ||||||
| import localeMultiDAnalysis from '@/views/visualization/multi-dimension-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 localeLoginLog from '@/views/monitor/log/login/locale/zh-CN'; | ||||||
| import localeOperationLog from '@/views/monitor/log/operation/locale/zh-CN'; | import localeOperationLog from '@/views/monitor/log/operation/locale/zh-CN'; | ||||||
| import localeSystemLog from '@/views/monitor/log/system/locale/zh-CN'; | import localeSystemLog from '@/views/monitor/log/system/locale/zh-CN'; | ||||||
| @@ -57,6 +58,7 @@ export default { | |||||||
|   ...localeDataAnalysis, |   ...localeDataAnalysis, | ||||||
|   ...localeMultiDAnalysis, |   ...localeMultiDAnalysis, | ||||||
|  |  | ||||||
|  |   ...localeOnlineUser, | ||||||
|   ...localeLoginLog, |   ...localeLoginLog, | ||||||
|   ...localeOperationLog, |   ...localeOperationLog, | ||||||
|   ...localeSystemLog, |   ...localeSystemLog, | ||||||
|   | |||||||
| @@ -12,6 +12,16 @@ const Monitor: AppRouteRecordRaw = { | |||||||
|     order: 2, |     order: 2, | ||||||
|   }, |   }, | ||||||
|   children: [ |   children: [ | ||||||
|  |     { | ||||||
|  |       path: '/online', | ||||||
|  |       name: 'OnlineUser', | ||||||
|  |       component: () => import('@/views/monitor/online/index.vue'), | ||||||
|  |       meta: { | ||||||
|  |         locale: 'menu.online.user.list', | ||||||
|  |         requiresAuth: true, | ||||||
|  |         roles: ['*'], | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       path: 'log/login', |       path: 'log/login', | ||||||
|       name: 'LoginLog', |       name: 'LoginLog', | ||||||
|   | |||||||
| @@ -4,15 +4,8 @@ | |||||||
|     <a-card class="general-card" :title="$t('menu.log.login.list')"> |     <a-card class="general-card" :title="$t('menu.log.login.list')"> | ||||||
|       <a-row style="margin-bottom: 15px"> |       <a-row style="margin-bottom: 15px"> | ||||||
|         <a-col :span="24"> |         <a-col :span="24"> | ||||||
|           <a-form |           <a-form ref="queryFormRef" :model="queryFormData" layout="inline"> | ||||||
|             ref="queryFormRef" |             <a-form-item field="status" hide-label> | ||||||
|             :model="queryFormData" |  | ||||||
|             layout="inline" |  | ||||||
|           > |  | ||||||
|             <a-form-item |  | ||||||
|               field="status" |  | ||||||
|               hide-label |  | ||||||
|             > |  | ||||||
|               <a-select |               <a-select | ||||||
|                 v-model="queryFormData.status" |                 v-model="queryFormData.status" | ||||||
|                 :options="statusOptions" |                 :options="statusOptions" | ||||||
| @@ -21,10 +14,7 @@ | |||||||
|                 style="width: 150px;" |                 style="width: 150px;" | ||||||
|               /> |               /> | ||||||
|             </a-form-item> |             </a-form-item> | ||||||
|             <a-form-item |             <a-form-item field="createTime" hide-label> | ||||||
|               field="createTime" |  | ||||||
|               hide-label |  | ||||||
|             > |  | ||||||
|               <date-range-picker v-model="queryFormData.createTime" /> |               <date-range-picker v-model="queryFormData.createTime" /> | ||||||
|             </a-form-item> |             </a-form-item> | ||||||
|             <a-button type="primary" @click="toQuery"> |             <a-button type="primary" @click="toQuery"> | ||||||
|   | |||||||
| @@ -4,15 +4,8 @@ | |||||||
|     <a-card class="general-card" :title="$t('menu.log.operation.list')"> |     <a-card class="general-card" :title="$t('menu.log.operation.list')"> | ||||||
|       <a-row style="margin-bottom: 15px"> |       <a-row style="margin-bottom: 15px"> | ||||||
|         <a-col :span="24"> |         <a-col :span="24"> | ||||||
|           <a-form |           <a-form ref="queryFormRef" :model="queryFormData" layout="inline"> | ||||||
|             ref="queryFormRef" |             <a-form-item field="description" hide-label> | ||||||
|             :model="queryFormData" |  | ||||||
|             layout="inline" |  | ||||||
|           > |  | ||||||
|             <a-form-item |  | ||||||
|               field="description" |  | ||||||
|               hide-label |  | ||||||
|             > |  | ||||||
|               <a-input |               <a-input | ||||||
|                 v-model="queryFormData.description" |                 v-model="queryFormData.description" | ||||||
|                 placeholder="输入操作内容搜索" |                 placeholder="输入操作内容搜索" | ||||||
| @@ -21,10 +14,7 @@ | |||||||
|                 @press-enter="toQuery" |                 @press-enter="toQuery" | ||||||
|               /> |               /> | ||||||
|             </a-form-item> |             </a-form-item> | ||||||
|             <a-form-item |             <a-form-item field="status" hide-label> | ||||||
|               field="status" |  | ||||||
|               hide-label |  | ||||||
|             > |  | ||||||
|               <a-select |               <a-select | ||||||
|                 v-model="queryFormData.status" |                 v-model="queryFormData.status" | ||||||
|                 :options="statusOptions" |                 :options="statusOptions" | ||||||
| @@ -33,10 +23,7 @@ | |||||||
|                 style="width: 150px;" |                 style="width: 150px;" | ||||||
|               /> |               /> | ||||||
|             </a-form-item> |             </a-form-item> | ||||||
|             <a-form-item |             <a-form-item field="createTime" hide-label> | ||||||
|               field="createTime" |  | ||||||
|               hide-label |  | ||||||
|             > |  | ||||||
|               <date-range-picker v-model="queryFormData.createTime" /> |               <date-range-picker v-model="queryFormData.createTime" /> | ||||||
|             </a-form-item> |             </a-form-item> | ||||||
|             <a-button type="primary" @click="toQuery"> |             <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