refactor: 使用事件解耦业务副作用并优化虚拟线程并发

- 新增登录成功事件,解耦在线用户缓存、登录日志和最近登录信息更新
  - 新增在线用户清理事件,角色授权变更后异步清理受影响用户会话
  - 新增 OSS 配置变更事件,统一处理配置初始化、删除和默认配置切换后的缓存刷新
  - 新增工作流抄送、待办消息、结果消息事件,拆分全局监听器中的副作用逻辑
  - 扩展 ThreadUtils 支持带返回值的虚拟线程批量执行
  - 在线用户监控批量读取 Redis 会话信息时使用虚拟线程并发处理
  - OSS 多文件查询、URL 解析和 DTO 转换使用虚拟线程并发处理
  - 工作流办理人回显和用户解析按类型并发查询
  - 工作流多渠道消息发送改为并发执行
  - WebSocket 与 SSE 广播消息按用户并发发送
This commit is contained in:
疯狂的狮子Li
2026-05-16 18:44:24 +08:00
parent f171ac03c4
commit 84afd6e6c0
23 changed files with 579 additions and 146 deletions
@@ -0,0 +1,13 @@
package org.dromara.web.event;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
/**
* 用户登录成功事件。
*
* @param loginId 登录标识
* @param tokenValue token 值
* @param loginParameter 登录参数
*/
public record UserLoginSuccessEvent(Object loginId, String tokenValue, SaLoginParameter loginParameter) {
}
@@ -2,73 +2,29 @@ package org.dromara.web.listener;
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ip.AddressUtils;
import org.dromara.common.log.event.LoginInfoEvent;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.api.domain.UserOnlineDTO;
import org.dromara.web.service.SysLoginService;
import org.dromara.web.event.UserLoginSuccessEvent;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* 用户行为监听器,用于同步在线状态和登录日志。
*
* @author Lion Li
*/
@RequiredArgsConstructor
@Component
@Slf4j
public class UserActionListener implements SaTokenListener {
private final SysLoginService loginService;
/**
* 登录成功后记录在线信息并写入登录日志。
*/
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtils.getClientIP();
UserOnlineDTO dto = new UserOnlineDTO();
dto.setIpaddr(ip);
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
dto.setBrowser(userAgent.getBrowser().getName());
dto.setOs(userAgent.getOs().getName());
dto.setLoginTime(System.currentTimeMillis());
dto.setTokenId(tokenValue);
String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY);
dto.setUserName(username);
dto.setClientKey((String) loginParameter.getExtra(LoginHelper.CLIENT_KEY));
dto.setDeviceType(loginParameter.getDeviceType());
dto.setDeptName((String) loginParameter.getExtra(LoginHelper.DEPT_NAME_KEY));
if (loginParameter.getTimeout() == -1) {
RedisUtils.setCacheObject(CacheNames.ONLINE_TOKEN_KEY + tokenValue, dto);
} else {
RedisUtils.setCacheObject(CacheNames.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(loginParameter.getTimeout()));
}
// 记录登录日志
LoginInfoEvent loginInfoEvent = new LoginInfoEvent();
loginInfoEvent.setUsername(username);
loginInfoEvent.setStatus(Constants.LOGIN_SUCCESS);
loginInfoEvent.setMessage(MessageUtils.message("user.login.success"));
loginInfoEvent.setRequest(ServletUtils.getRequest());
SpringUtils.context().publishEvent(loginInfoEvent);
// 更新登录信息
loginService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip);
log.info("user doLogin, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8));
SpringUtils.context().publishEvent(new UserLoginSuccessEvent(loginId, tokenValue, loginParameter));
}
/**
@@ -0,0 +1,71 @@
package org.dromara.web.listener;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ip.AddressUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.api.domain.UserOnlineDTO;
import org.dromara.web.event.UserLoginSuccessEvent;
import org.dromara.web.service.SysLoginService;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* 用户登录成功监听器。
*
* @author Lion Li
*/
@RequiredArgsConstructor
@Component
@Slf4j
public class UserLoginSuccessListener {
private final SysLoginService loginService;
/**
* 登录成功后记录在线信息、登录日志与最近登录信息。
*
* @param event 用户登录成功事件
*/
@EventListener
public void handleLoginSuccess(UserLoginSuccessEvent event) {
SaLoginParameter loginParameter = event.loginParameter();
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtils.getClientIP();
String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY);
String tokenValue = event.tokenValue();
UserOnlineDTO dto = new UserOnlineDTO();
dto.setIpaddr(ip);
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
dto.setBrowser(userAgent.getBrowser().getName());
dto.setOs(userAgent.getOs().getName());
dto.setLoginTime(System.currentTimeMillis());
dto.setTokenId(tokenValue);
dto.setUserName(username);
dto.setClientKey((String) loginParameter.getExtra(LoginHelper.CLIENT_KEY));
dto.setDeviceType(loginParameter.getDeviceType());
dto.setDeptName((String) loginParameter.getExtra(LoginHelper.DEPT_NAME_KEY));
if (loginParameter.getTimeout() == -1) {
RedisUtils.setCacheObject(CacheNames.ONLINE_TOKEN_KEY + tokenValue, dto);
} else {
RedisUtils.setCacheObject(CacheNames.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(loginParameter.getTimeout()));
}
loginService.recordLoginInfo(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
loginService.updateLastLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip);
log.info("user doLogin, userId:{}, token:***{}", event.loginId(), StringUtils.right(tokenValue, 8));
}
}
@@ -175,7 +175,7 @@ public class SysLoginService {
* @param userId 用户ID
* @param ip 登录IP
*/
public void recordLoginInfo(Long userId, String ip) {
public void updateLastLoginInfo(Long userId, String ip) {
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setLoginIp(ip);