Compare commits
2 Commits
ae32e6d953
...
35775b0fcc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35775b0fcc | ||
|
|
b689713e3a |
@ -0,0 +1,163 @@
|
|||||||
|
package org.leocoder.thin.system.controller.online;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.session.SaSession;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.leocoder.thin.common.constants.SaTokenSessionConstants;
|
||||||
|
import org.leocoder.thin.common.satoken.CoderLoginUser;
|
||||||
|
import org.leocoder.thin.domain.model.vo.system.SysUserOnlineVo;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Leocoder
|
||||||
|
* @description [在线用户管理]
|
||||||
|
*/
|
||||||
|
@Tag(name = "在线用户管理", description = "实时查看在线用户、强制注销等功能")
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/coder")
|
||||||
|
public class SysUserOnlineController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [分页查询在线用户列表]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Operation(summary = "分页查询在线用户", description = "实时获取所有在线用户信息,支持按登录名、用户名、IP地址过滤")
|
||||||
|
@SaCheckPermission("monitor:online:list")
|
||||||
|
@GetMapping("/sysUserOnline/listPage")
|
||||||
|
public Map<String, Object> listPage(SysUserOnlineVo vo) {
|
||||||
|
// 使用Sa-Token官方API获取所有已登录的会话ID
|
||||||
|
// keyword: 查询关键字,只有包括这个字符串的 token 值才会被查询出来。
|
||||||
|
// start: 数据开始处索引。
|
||||||
|
// size: 要获取的数据条数 (值为-1代表一直获取到末尾)。
|
||||||
|
// sortType: 排序方式(true=正序:先登录的在前,false=反序:后登录的在前)。
|
||||||
|
List<String> sessionKeyList = StpUtil.searchSessionId("", 0, -1, false);
|
||||||
|
List<CoderLoginUser> loginUserList = null;
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
if (CollectionUtil.isNotEmpty(sessionKeyList)) {
|
||||||
|
loginUserList = new ArrayList<>();
|
||||||
|
|
||||||
|
// 遍历所有会话,获取登录用户信息
|
||||||
|
for (String sessionKey : sessionKeyList) {
|
||||||
|
try {
|
||||||
|
// 使用sessionKey获取SaSession对象
|
||||||
|
SaSession saSession = StpUtil.getSessionBySessionId(sessionKey);
|
||||||
|
if (saSession != null) {
|
||||||
|
// 获取登录用户对象
|
||||||
|
CoderLoginUser coderLoginUser = saSession.getModel(
|
||||||
|
SaTokenSessionConstants.LOGIN_USER,
|
||||||
|
CoderLoginUser.class
|
||||||
|
);
|
||||||
|
if (coderLoginUser != null) {
|
||||||
|
loginUserList.add(coderLoginUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 处理可能的会话过期或异常,避免影响整体查询
|
||||||
|
log.warn("获取会话[{}]用户信息失败: {}", sessionKey, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取查询参数
|
||||||
|
Integer pageNo = vo.getPageNo() != null ? vo.getPageNo() : 1;
|
||||||
|
Integer pageSize = vo.getPageSize() != null ? vo.getPageSize() : 10;
|
||||||
|
String loginName = vo.getLoginName();
|
||||||
|
String userName = vo.getUserName();
|
||||||
|
String loginIp = vo.getLoginIp();
|
||||||
|
|
||||||
|
// 根据条件过滤用户列表
|
||||||
|
List<CoderLoginUser> filteredList = loginUserList.stream()
|
||||||
|
.filter(loginUser -> userName == null || userName.isEmpty() ||
|
||||||
|
(loginUser.getUserName() != null && loginUser.getUserName().contains(userName)))
|
||||||
|
.filter(loginUser -> loginName == null || loginName.isEmpty() ||
|
||||||
|
(loginUser.getLoginName() != null && loginUser.getLoginName().contains(loginName)))
|
||||||
|
.filter(loginUser -> loginIp == null || loginIp.isEmpty() ||
|
||||||
|
(loginUser.getLoginIp() != null && loginUser.getLoginIp().contains(loginIp)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 计算分页信息
|
||||||
|
int totalCount = filteredList.size();
|
||||||
|
int totalPages = (int) Math.ceil((double) totalCount / pageSize);
|
||||||
|
int startIndex = (pageNo - 1) * pageSize;
|
||||||
|
int endIndex = Math.min(startIndex + pageSize, totalCount);
|
||||||
|
|
||||||
|
// 执行分页
|
||||||
|
List<CoderLoginUser> pagedList = filteredList.stream()
|
||||||
|
.skip(startIndex)
|
||||||
|
.limit(pageSize)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 构建返回结果
|
||||||
|
result.put("total", totalCount);
|
||||||
|
result.put("current", pageNo);
|
||||||
|
result.put("size", pageSize);
|
||||||
|
result.put("pages", totalPages);
|
||||||
|
result.put("records", pagedList);
|
||||||
|
|
||||||
|
log.info("查询在线用户成功,总数: {}, 过滤后: {}, 当前页: {}",
|
||||||
|
loginUserList.size(), totalCount, pagedList.size());
|
||||||
|
} else {
|
||||||
|
// 没有在线用户
|
||||||
|
result.put("total", 0);
|
||||||
|
result.put("current", vo.getPageNo() != null ? vo.getPageNo() : 1);
|
||||||
|
result.put("size", vo.getPageSize() != null ? vo.getPageSize() : 10);
|
||||||
|
result.put("pages", 0);
|
||||||
|
result.put("records", Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [强制注销]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Operation(summary = "强制注销", description = "强制指定用户注销登录,清除Token信息")
|
||||||
|
@SaCheckPermission("monitor:online:logout")
|
||||||
|
@GetMapping("/sysUserOnline/logout/{userId}")
|
||||||
|
public String logout(@PathVariable("userId") Long userId) {
|
||||||
|
try {
|
||||||
|
// 使用Sa-Token强制指定账号注销下线
|
||||||
|
// 注销会清除该用户的所有Token信息
|
||||||
|
StpUtil.logout(userId);
|
||||||
|
log.info("用户[{}]已被强制注销", userId);
|
||||||
|
return "强制注销成功";
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("强制注销失败,用户ID: {}, 错误: {}", userId, e.getMessage());
|
||||||
|
throw new RuntimeException("强制注销失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [获取在线用户统计信息]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Operation(summary = "获取在线用户统计", description = "获取当前在线用户总数等统计信息")
|
||||||
|
@SaCheckPermission("monitor:online:list")
|
||||||
|
@GetMapping("/sysUserOnline/count")
|
||||||
|
public Map<String, Object> getOnlineUserCount() {
|
||||||
|
List<String> sessionKeyList = StpUtil.searchSessionId("", 0, -1, false);
|
||||||
|
int onlineCount = sessionKeyList != null ? sessionKeyList.size() : 0;
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("onlineCount", onlineCount);
|
||||||
|
result.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
log.info("当前在线用户数: {}", onlineCount);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
93
sql/20250927-sys_user_online.sql
Normal file
93
sql/20250927-sys_user_online.sql
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* 在线用户管理功能SQL脚本
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* 1. 基于Sa-Token + Redis实现,无需专门的在线用户表
|
||||||
|
* 2. 仅需菜单权限配置,用户数据完全存储在Redis会话中
|
||||||
|
* 3. 支持实时查看在线用户、踢人下线、强制注销等功能
|
||||||
|
*
|
||||||
|
* 作者:Leocoder
|
||||||
|
* 创建时间:2025-09-27
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 在线用户管理菜单权限配置
|
||||||
|
-- ----------------------------
|
||||||
|
|
||||||
|
-- 1. 系统监控菜单(如果不存在)
|
||||||
|
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `en_name`, `parent_id`, `menu_type`, `path`, `name`, `component`, `icon`, `auth`, `menu_status`, `active_menu`, `is_hide`, `is_link`, `is_keep_alive`, `is_full`, `is_affix`, `is_spread`, `sorted`, `create_by`, `create_time`, `update_by`, `update_time`)
|
||||||
|
SELECT 2000, '系统监控', 'System Monitor', 0, '1', '/monitor', 'monitorPage', '', 'Monitor', 'monitor:auth', '0', NULL, '1', '', '0', '1', '1', '1', 3, 'system', NOW(), 'system', NOW()
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `menu_id` = 2000);
|
||||||
|
|
||||||
|
-- 2. 在线用户菜单
|
||||||
|
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `en_name`, `parent_id`, `menu_type`, `path`, `name`, `component`, `icon`, `auth`, `menu_status`, `active_menu`, `is_hide`, `is_link`, `is_keep_alive`, `is_full`, `is_affix`, `is_spread`, `sorted`, `create_by`, `create_time`, `update_by`, `update_time`)
|
||||||
|
VALUES (2001, '在线用户', 'Online Users', 2000, '2', '/monitor/online', 'onlineUsersPage', 'monitor/online/index', 'UserFilled', 'monitor:online:list', '0', NULL, '1', '', '1', '1', '1', '1', 1, 'system', NOW(), 'system', NOW());
|
||||||
|
|
||||||
|
-- 3. 在线用户-查看权限
|
||||||
|
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `en_name`, `parent_id`, `menu_type`, `path`, `name`, `component`, `icon`, `auth`, `menu_status`, `active_menu`, `is_hide`, `is_link`, `is_keep_alive`, `is_full`, `is_affix`, `is_spread`, `sorted`, `create_by`, `create_time`, `update_by`, `update_time`)
|
||||||
|
VALUES (2002, '查看', 'View', 2001, '3', '', NULL, NULL, '', 'monitor:online:list', '0', NULL, '0', '', '0', '1', '1', '1', 1, 'system', NOW(), 'system', NOW());
|
||||||
|
|
||||||
|
-- 4. 在线用户-踢人下线权限
|
||||||
|
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `en_name`, `parent_id`, `menu_type`, `path`, `name`, `component`, `icon`, `auth`, `menu_status`, `active_menu`, `is_hide`, `is_link`, `is_keep_alive`, `is_full`, `is_affix`, `is_spread`, `sorted`, `create_by`, `create_time`, `update_by`, `update_time`)
|
||||||
|
VALUES (2003, '踢人下线', 'Kick Out', 2001, '3', '', NULL, NULL, '', 'monitor:online:kickout', '0', NULL, '0', '', '0', '1', '1', '1', 2, 'system', NOW(), 'system', NOW());
|
||||||
|
|
||||||
|
-- 5. 在线用户-强制注销权限
|
||||||
|
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `en_name`, `parent_id`, `menu_type`, `path`, `name`, `component`, `icon`, `auth`, `menu_status`, `active_menu`, `is_hide`, `is_link`, `is_keep_alive`, `is_full`, `is_affix`, `is_spread`, `sorted`, `create_by`, `create_time`, `update_by`, `update_time`)
|
||||||
|
VALUES (2004, '强制注销', 'Force Logout', 2001, '3', '', NULL, NULL, '', 'monitor:online:logout', '0', NULL, '0', '', '0', '1', '1', '1', 3, 'system', NOW(), 'system', NOW());
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 角色权限关联(管理员角色拥有在线用户管理权限)
|
||||||
|
-- ----------------------------
|
||||||
|
|
||||||
|
-- 给管理员角色分配在线用户菜单权限
|
||||||
|
-- 注意:这里假设管理员角色ID为1,请根据实际情况调整
|
||||||
|
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
|
||||||
|
SELECT 1, 2000 WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 2000);
|
||||||
|
|
||||||
|
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
|
||||||
|
SELECT 1, 2001 WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 2001);
|
||||||
|
|
||||||
|
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
|
||||||
|
SELECT 1, 2002 WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 2002);
|
||||||
|
|
||||||
|
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
|
||||||
|
SELECT 1, 2003 WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 2003);
|
||||||
|
|
||||||
|
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
|
||||||
|
SELECT 1, 2004 WHERE NOT EXISTS (SELECT 1 FROM `sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 2004);
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 执行说明
|
||||||
|
-- ----------------------------
|
||||||
|
|
||||||
|
/*
|
||||||
|
执行此脚本后,系统将具备以下功能:
|
||||||
|
|
||||||
|
1. 🎯 功能特性:
|
||||||
|
✅ 实时查看所有在线用户
|
||||||
|
✅ 显示用户详细信息(IP、设备、浏览器等)
|
||||||
|
✅ 支持按登录名、用户名、IP地址过滤
|
||||||
|
✅ 分页显示
|
||||||
|
✅ 踢人下线功能
|
||||||
|
✅ 强制注销功能
|
||||||
|
|
||||||
|
2. 🔐 权限控制:
|
||||||
|
✅ monitor:online:list - 查看在线用户列表
|
||||||
|
✅ monitor:online:kickout - 踢人下线权限
|
||||||
|
✅ monitor:online:logout - 强制注销权限
|
||||||
|
|
||||||
|
3. 🏗️ 实现原理:
|
||||||
|
✅ 基于Sa-Token会话管理
|
||||||
|
✅ 数据存储在Redis中
|
||||||
|
✅ 无需额外数据表
|
||||||
|
✅ 实时性能优秀
|
||||||
|
|
||||||
|
4. 📱 前端菜单路径:
|
||||||
|
✅ /monitor/online - 在线用户管理页面
|
||||||
|
✅ 位于系统监控模块下
|
||||||
|
|
||||||
|
注意事项:
|
||||||
|
- 确保系统已集成Sa-Token和Redis
|
||||||
|
- 确保角色权限表存在且管理员角色ID正确
|
||||||
|
- 执行前请备份相关表数据
|
||||||
|
*/
|
||||||
Loading…
Reference in New Issue
Block a user