diff --git a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/online/SysUserOnlineController.java b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/online/SysUserOnlineController.java new file mode 100644 index 0000000..bad6f35 --- /dev/null +++ b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/online/SysUserOnlineController.java @@ -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 listPage(SysUserOnlineVo vo) { + // 使用Sa-Token官方API获取所有已登录的会话ID + // keyword: 查询关键字,只有包括这个字符串的 token 值才会被查询出来。 + // start: 数据开始处索引。 + // size: 要获取的数据条数 (值为-1代表一直获取到末尾)。 + // sortType: 排序方式(true=正序:先登录的在前,false=反序:后登录的在前)。 + List sessionKeyList = StpUtil.searchSessionId("", 0, -1, false); + List loginUserList = null; + Map 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 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 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 getOnlineUserCount() { + List sessionKeyList = StpUtil.searchSessionId("", 0, -1, false); + int onlineCount = sessionKeyList != null ? sessionKeyList.size() : 0; + + Map result = new HashMap<>(); + result.put("onlineCount", onlineCount); + result.put("timestamp", System.currentTimeMillis()); + + log.info("当前在线用户数: {}", onlineCount); + return result; + } +} \ No newline at end of file