feat(monitor): 实现在线用户管理Controller

- 实现分页查询在线用户列表功能
- 支持按登录名、用户名、IP地址过滤
- 实现强制注销功能,彻底清除用户Token
- 新增在线用户统计接口
- 基于Sa-Token API实现会话管理
- 完善异常处理和日志记录
- 遵循统一的权限验证和接口规范
This commit is contained in:
Leo 2025-09-27 17:51:31 +08:00
parent b689713e3a
commit 35775b0fcc

View File

@ -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;
}
}