From 3116fd3eaec28911a0ec252b83a10a42c0661a78 Mon Sep 17 00:00:00 2001 From: Charles7c <charles7c@126.com> Date: Sat, 21 Jan 2023 14:15:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=9B=91=E6=8E=A7/=E5=9C=A8=E7=BA=BF?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=83=A8=E5=88=86=E6=B3=A8=E9=87=8A=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +- .../handler/GlobalExceptionHandler.java | 15 +- .../cnadmin/common/model/dto/LoginUser.java | 25 +++ .../cnadmin/common/model/query/PageQuery.java | 4 + .../cnadmin/common/model/vo/PageInfo.java | 35 +++ .../common/util/helper/LoginHelper.java | 25 ++- .../cnadmin/monitor/model/entity/SysLog.java | 2 +- .../monitor/model/query/OnlineUserQuery.java | 52 +++++ .../cnadmin/monitor/model/vo/LoginLogVO.java | 8 +- .../monitor/model/vo/OnlineUserVO.java | 79 +++++++ .../monitor/model/vo/OperationLogVO.java | 4 +- .../monitor/model/vo/SystemLogDetailVO.java | 4 +- .../cnadmin/monitor/model/vo/SystemLogVO.java | 4 +- .../cnadmin/monitor/service/LogService.java | 2 +- continew-admin-ui/src/api/monitor/online.ts | 35 +++ continew-admin-ui/src/locale/en-US.ts | 2 + continew-admin-ui/src/locale/zh-CN.ts | 2 + .../src/router/routes/modules/monitor.ts | 10 + .../src/views/monitor/log/login/index.vue | 16 +- .../src/views/monitor/log/operation/index.vue | 21 +- .../src/views/monitor/online/index.vue | 202 ++++++++++++++++++ .../src/views/monitor/online/locale/en-US.ts | 3 + .../src/views/monitor/online/locale/zh-CN.ts | 3 + .../monitor/OnlineUserController.java | 126 +++++++++++ 24 files changed, 640 insertions(+), 48 deletions(-) create mode 100644 continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/query/OnlineUserQuery.java create mode 100644 continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/OnlineUserVO.java create mode 100644 continew-admin-ui/src/api/monitor/online.ts create mode 100644 continew-admin-ui/src/views/monitor/online/index.vue create mode 100644 continew-admin-ui/src/views/monitor/online/locale/en-US.ts create mode 100644 continew-admin-ui/src/views/monitor/online/locale/zh-CN.ts create mode 100644 continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/monitor/OnlineUserController.java diff --git a/README.md b/README.md index a929c809..851b81ae 100644 --- a/README.md +++ b/README.md @@ -251,10 +251,11 @@ continew-admin │ ├─ views # 页面模板 │ │ ├─ login # 登录模块 │ │ ├─ monitor # 系统监控模块 - │ │ │ └─ log # 日志管理 - │ │ │ ├─ login # 登录日志 - │ │ │ ├─ operation # 操作日志 - │ │ │ └─ system # 系统日志 + │ │ │ ├─ log # 日志管理 + │ │ │ │ ├─ login # 登录日志 + │ │ │ │ ├─ operation # 操作日志 + │ │ │ │ └─ system # 系统日志 + │ │ │ └─ online # 在线用户 │ │ └─ system # 系统管理模块 │ │ └─ user # 用户模块 │ │ └─ center # 个人中心 diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/handler/GlobalExceptionHandler.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/handler/GlobalExceptionHandler.java index 749a091d..b7da3af1 100644 --- a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/handler/GlobalExceptionHandler.java +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/handler/GlobalExceptionHandler.java @@ -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); } diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/dto/LoginUser.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/dto/LoginUser.java index 801b931d..368d0842 100644 --- a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/dto/LoginUser.java +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/dto/LoginUser.java @@ -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; } diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/query/PageQuery.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/query/PageQuery.java index 61fed926..a8643987 100644 --- a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/query/PageQuery.java +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/query/PageQuery.java @@ -78,6 +78,10 @@ public class PageQuery implements Serializable { this.size = DEFAULT_SIZE; } + public int getPage() { + return page < 0 ? DEFAULT_PAGE : page; + } + /** * 解析排序条件为 Spring 分页排序实体 * diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/vo/PageInfo.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/vo/PageInfo.java index b8ae4f58..88f4a90a 100644 --- a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/vo/PageInfo.java +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/vo/PageInfo.java @@ -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; + } } diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/helper/LoginHelper.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/helper/LoginHelper.java index 62655f93..38adc865 100644 --- a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/helper/LoginHelper.java +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/helper/LoginHelper.java @@ -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); } diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/entity/SysLog.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/entity/SysLog.java index 022d48df..8c27d263 100644 --- a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/entity/SysLog.java +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/entity/SysLog.java @@ -39,7 +39,7 @@ public class SysLog implements Serializable { private static final long serialVersionUID = 1L; /** - * 日志ID + * 日志 ID */ @TableId private Long logId; diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/query/OnlineUserQuery.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/query/OnlineUserQuery.java new file mode 100644 index 00000000..60164218 --- /dev/null +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/query/OnlineUserQuery.java @@ -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; +} diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/LoginLogVO.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/LoginLogVO.java index afbab97a..a9e6e615 100644 --- a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/LoginLogVO.java +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/LoginLogVO.java @@ -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; /** diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/OnlineUserVO.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/OnlineUserVO.java new file mode 100644 index 00000000..24b92a38 --- /dev/null +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/OnlineUserVO.java @@ -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; +} diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/OperationLogVO.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/OperationLogVO.java index 6b8f8719..0b0dbf4a 100644 --- a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/OperationLogVO.java +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/OperationLogVO.java @@ -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; /** diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/SystemLogDetailVO.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/SystemLogDetailVO.java index 7164828b..2c0285e8 100644 --- a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/SystemLogDetailVO.java +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/SystemLogDetailVO.java @@ -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; /** diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/SystemLogVO.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/SystemLogVO.java index 5df0b05e..8a6c11ef 100644 --- a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/SystemLogVO.java +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/vo/SystemLogVO.java @@ -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; /** diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/service/LogService.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/service/LogService.java index 7d0bf7df..84f3feea 100644 --- a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/service/LogService.java +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/service/LogService.java @@ -71,7 +71,7 @@ public interface LogService { * 查看系统日志详情 * * @param logId - * 日志ID + * 日志 ID * @return 系统日志详情 */ SystemLogDetailVO detail(Long logId); diff --git a/continew-admin-ui/src/api/monitor/online.ts b/continew-admin-ui/src/api/monitor/online.ts new file mode 100644 index 00000000..77501928 --- /dev/null +++ b/continew-admin-ui/src/api/monitor/online.ts @@ -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}`); +} \ No newline at end of file diff --git a/continew-admin-ui/src/locale/en-US.ts b/continew-admin-ui/src/locale/en-US.ts index 2491c3ce..d33820e0 100644 --- a/continew-admin-ui/src/locale/en-US.ts +++ b/continew-admin-ui/src/locale/en-US.ts @@ -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, diff --git a/continew-admin-ui/src/locale/zh-CN.ts b/continew-admin-ui/src/locale/zh-CN.ts index 27fce54a..c200ef44 100644 --- a/continew-admin-ui/src/locale/zh-CN.ts +++ b/continew-admin-ui/src/locale/zh-CN.ts @@ -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, diff --git a/continew-admin-ui/src/router/routes/modules/monitor.ts b/continew-admin-ui/src/router/routes/modules/monitor.ts index 9ad616ff..0bd5bd1b 100644 --- a/continew-admin-ui/src/router/routes/modules/monitor.ts +++ b/continew-admin-ui/src/router/routes/modules/monitor.ts @@ -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', diff --git a/continew-admin-ui/src/views/monitor/log/login/index.vue b/continew-admin-ui/src/views/monitor/log/login/index.vue index f68dede2..3eee355b 100644 --- a/continew-admin-ui/src/views/monitor/log/login/index.vue +++ b/continew-admin-ui/src/views/monitor/log/login/index.vue @@ -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"> diff --git a/continew-admin-ui/src/views/monitor/log/operation/index.vue b/continew-admin-ui/src/views/monitor/log/operation/index.vue index bb55b582..afef8e9f 100644 --- a/continew-admin-ui/src/views/monitor/log/operation/index.vue +++ b/continew-admin-ui/src/views/monitor/log/operation/index.vue @@ -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"> diff --git a/continew-admin-ui/src/views/monitor/online/index.vue b/continew-admin-ui/src/views/monitor/online/index.vue new file mode 100644 index 00000000..5e0e43c7 --- /dev/null +++ b/continew-admin-ui/src/views/monitor/online/index.vue @@ -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> diff --git a/continew-admin-ui/src/views/monitor/online/locale/en-US.ts b/continew-admin-ui/src/views/monitor/online/locale/en-US.ts new file mode 100644 index 00000000..ea23c972 --- /dev/null +++ b/continew-admin-ui/src/views/monitor/online/locale/en-US.ts @@ -0,0 +1,3 @@ +export default { + 'menu.online.user.list': 'Online user', +}; diff --git a/continew-admin-ui/src/views/monitor/online/locale/zh-CN.ts b/continew-admin-ui/src/views/monitor/online/locale/zh-CN.ts new file mode 100644 index 00000000..bab3974c --- /dev/null +++ b/continew-admin-ui/src/views/monitor/online/locale/zh-CN.ts @@ -0,0 +1,3 @@ +export default { + 'menu.online.user.list': '在线用户', +}; diff --git a/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/monitor/OnlineUserController.java b/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/monitor/OnlineUserController.java new file mode 100644 index 00000000..5a4282b0 --- /dev/null +++ b/continew-admin-webapi/src/main/java/top/charles7c/cnadmin/webapi/controller/monitor/OnlineUserController.java @@ -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("强退成功"); + } +}