package jnpf.base.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import jnpf.base.LogSortEnum; import jnpf.base.UserInfo; import jnpf.base.entity.LogEntity; import jnpf.base.entity.LoginSessionEntity; import jnpf.base.mapper.LogMapper; import jnpf.base.mapper.LoginSessionMapper; import jnpf.base.model.logmodel.LoginSessionEventModel; import jnpf.base.model.logmodel.LoginSessionEventResult; import jnpf.base.model.logmodel.LoginSessionStatus; import jnpf.base.model.logmodel.LogoutReason; import jnpf.base.service.LoginSessionService; import jnpf.base.service.SuperServiceImpl; import jnpf.util.DateUtil; import jnpf.util.RandomUtil; import jnpf.util.RedisUtil; import jnpf.util.StringUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.List; @Service @RequiredArgsConstructor public class LoginSessionServiceImpl extends SuperServiceImpl implements LoginSessionService { private static final String TOKEN_MAPPING_PREFIX = "jnpf:login-session:token:"; private static final int SINGLE_LOGIN_MODE = 1; private static final int CONCURRENT_LOGIN_MODE = 2; private final LogMapper logMapper; private final RedisUtil redisUtil; @Override @Transactional(rollbackFor = Exception.class) public LoginSessionEventResult record(LoginSessionEventModel event) { if (event == null || StringUtil.isEmpty(event.getSessionId()) || StringUtil.isEmpty(event.getEventType())) { return LoginSessionEventResult.fail(event == null ? null : event.getSessionId(), "登录会话事件参数不完整"); } switch (event.getEventType()) { case LoginSessionEventModel.OPEN: return open(event); case LoginSessionEventModel.CLOSE: return close(event); case LoginSessionEventModel.TOUCH: return touch(event); default: return LoginSessionEventResult.fail(event.getSessionId(), "不支持的登录会话事件类型"); } } @Override @Transactional(rollbackFor = Exception.class) public int closeExpired(Date beforeTime, int limit) { QueryWrapper query = new QueryWrapper<>(); query.lambda() .eq(LoginSessionEntity::getStatus, LoginSessionStatus.OPEN) .isNotNull(LoginSessionEntity::getExpireTime) .lt(LoginSessionEntity::getExpireTime, beforeTime) .orderByAsc(LoginSessionEntity::getExpireTime); Page page = new Page<>(1, Math.max(1, Math.min(limit, 500)), false); List sessions = this.baseMapper.selectPage(page, query).getRecords(); int closed = 0; for (LoginSessionEntity session : sessions) { if (redisUtil.exists(TOKEN_MAPPING_PREFIX + session.getId())) { continue; } LoginSessionEventModel event = LoginSessionEventModel.builder() .eventId(RandomUtil.uuId()) .transitionId(RandomUtil.uuId()) .eventType(LoginSessionEventModel.CLOSE) .sessionId(session.getId()) .logoutReason(LogoutReason.TIMEOUT) .description("登录超时") .eventTime(session.getExpireTime()) .build(); String logId = closeExisting(session, event, LogoutReason.TIMEOUT, "登录超时", session.getExpireTime()); if (StringUtil.isNotEmpty(logId)) { closed++; } } return closed; } private LoginSessionEventResult open(LoginSessionEventModel event) { LoginSessionEntity existing = this.baseMapper.selectById(event.getSessionId()); if (existing != null) { return LoginSessionEventResult.success(existing.getId(), existing.getLoginLogId()); } UserInfo userInfo = event.getUserInfo(); Date eventTime = eventTime(event); int loginMode = resolveLoginMode(event.getLoginMode()); if (loginMode == SINGLE_LOGIN_MODE && userInfo != null && StringUtil.isNotEmpty(userInfo.getUserId())) { QueryWrapper query = new QueryWrapper<>(); query.lambda() .eq(StringUtil.isNotEmpty(userInfo.getTenantId()), LoginSessionEntity::getTenantId, userInfo.getTenantId()) .isNull(StringUtil.isEmpty(userInfo.getTenantId()), LoginSessionEntity::getTenantId) .eq(LoginSessionEntity::getUserId, userInfo.getUserId()) .eq(LoginSessionEntity::getStatus, LoginSessionStatus.OPEN); query.last("FOR UPDATE"); List openSessions = this.baseMapper.selectList(query); for (LoginSessionEntity oldSession : openSessions) { closeExisting(oldSession, event, LogoutReason.REPLACED, "登录顶替", eventTime); } } String loginLogId = RandomUtil.uuId(); LogEntity loginLog = buildLog(event, loginLogId, 0, null, null, 2, "登录成功"); logMapper.insert(loginLog); LoginSessionEntity session = new LoginSessionEntity(); session.setId(event.getSessionId()); fillUser(session, userInfo); session.setLoginMode(loginMode); session.setStatus(LoginSessionStatus.OPEN); session.setLoginLogId(loginLogId); session.setLoginTime(eventTime); session.setLastActiveTime(eventTime); session.setExpireTime(event.getExpireTime()); session.setSource(StringUtil.isEmpty(event.getSource()) ? "NEW_LOGIN" : event.getSource()); session.setVersion(0); session.setCreatorTime(eventTime); session.setCreatorUserId(userInfo == null ? null : userInfo.getUserId()); this.baseMapper.insert(session); return LoginSessionEventResult.success(event.getSessionId(), loginLogId); } private LoginSessionEventResult close(LoginSessionEventModel event) { LoginSessionEntity session = this.baseMapper.selectById(event.getSessionId()); if (session == null) { session = createLegacySession(event); } if (LoginSessionStatus.CLOSED.equals(session.getStatus())) { return LoginSessionEventResult.success(session.getId(), session.getLogoutLogId()); } String reason = StringUtil.isEmpty(event.getLogoutReason()) ? LogoutReason.UNKNOWN_RECOVERY : event.getLogoutReason(); String logoutLogId = closeExisting(session, event, reason, event.getDescription(), eventTime(event)); return LoginSessionEventResult.success(event.getSessionId(), logoutLogId); } private LoginSessionEventResult touch(LoginSessionEventModel event) { UpdateWrapper update = new UpdateWrapper<>(); update.lambda() .eq(LoginSessionEntity::getId, event.getSessionId()) .eq(LoginSessionEntity::getStatus, LoginSessionStatus.OPEN) .set(LoginSessionEntity::getLastActiveTime, eventTime(event)) .set(event.getExpireTime() != null, LoginSessionEntity::getExpireTime, event.getExpireTime()) .setSql("f_version = f_version + 1"); int updated = this.baseMapper.update(null, update); if (updated == 0 && this.baseMapper.selectById(event.getSessionId()) == null && event.getUserInfo() != null) { createLegacySession(event); } return LoginSessionEventResult.success(event.getSessionId(), null); } private LoginSessionEntity createLegacySession(LoginSessionEventModel event) { UserInfo userInfo = event.getUserInfo(); int loginMode = resolveLoginMode(event.getLoginMode()); Date loginTime = parseLoginTime(userInfo, eventTime(event)); Date expireTime = event.getExpireTime() != null ? event.getExpireTime() : (userInfo == null ? null : userInfo.getOverdueTime()); String loginLogId = RandomUtil.uuId(); LoginSessionEventModel adopted = LoginSessionEventModel.builder() .eventId(event.getEventId()) .transitionId(event.getTransitionId()) .eventType(LoginSessionEventModel.OPEN) .sessionId(event.getSessionId()) .userInfo(userInfo) .loginMode(loginMode) .source("LEGACY_ADOPTED") .eventTime(loginTime) .expireTime(expireTime) .ipAddress(event.getIpAddress()) .ipAddressName(event.getIpAddressName()) .browser(event.getBrowser()) .platform(event.getPlatform()) .requestUrl(event.getRequestUrl()) .requestMethod(event.getRequestMethod()) .build(); LogEntity loginLog = buildLog(adopted, loginLogId, 0, null, null, 2, "存量会话接管"); logMapper.insert(loginLog); LoginSessionEntity session = new LoginSessionEntity(); session.setId(event.getSessionId()); fillUser(session, userInfo); session.setLoginMode(loginMode); session.setStatus(LoginSessionStatus.OPEN); session.setLoginLogId(loginLogId); session.setLoginTime(loginTime); session.setLastActiveTime(loginTime); session.setExpireTime(expireTime); session.setSource("LEGACY_ADOPTED"); session.setVersion(0); session.setCreatorTime(loginTime); session.setCreatorUserId(userInfo == null ? null : userInfo.getUserId()); this.baseMapper.insert(session); return session; } private int resolveLoginMode(Integer loginMode) { if (loginMode == null) { return SINGLE_LOGIN_MODE; } if (loginMode != SINGLE_LOGIN_MODE && loginMode != CONCURRENT_LOGIN_MODE) { throw new IllegalArgumentException("不支持的登录模式: " + loginMode); } return loginMode; } private String closeExisting(LoginSessionEntity session, LoginSessionEventModel event, String reason, String description, Date logoutTime) { String logoutLogId = RandomUtil.uuId(); UpdateWrapper update = new UpdateWrapper<>(); update.lambda() .eq(LoginSessionEntity::getId, session.getId()) .eq(LoginSessionEntity::getStatus, LoginSessionStatus.OPEN) .set(LoginSessionEntity::getStatus, LoginSessionStatus.CLOSED) .set(LoginSessionEntity::getLogoutLogId, logoutLogId) .set(LoginSessionEntity::getLogoutTime, logoutTime) .set(LoginSessionEntity::getLogoutReason, reason) .set(LoginSessionEntity::getLogoutDescription, description) .set(LoginSessionEntity::getLastModifyTime, logoutTime) .setSql("f_version = f_version + 1"); if (this.baseMapper.update(null, update) == 0) { LoginSessionEntity closed = this.baseMapper.selectById(session.getId()); return closed == null ? null : closed.getLogoutLogId(); } LoginSessionEventModel closeEvent = event; if (event.getUserInfo() == null || !session.getId().equals(event.getSessionId())) { LogEntity loginLog = logMapper.selectById(session.getLoginLogId()); UserInfo snapshot = new UserInfo(); snapshot.setUserId(session.getUserId()); snapshot.setUserName(session.getUserName()); snapshot.setTenantId(session.getTenantId()); snapshot.setLoginDevice(session.getDeviceType()); snapshot.setGrantType(session.getGrantType()); closeEvent = LoginSessionEventModel.builder() .eventId(event.getEventId()) .transitionId(event.getTransitionId()) .eventType(event.getEventType()) .sessionId(session.getId()) .userInfo(snapshot) .eventTime(logoutTime) .ipAddress(loginLog == null ? null : loginLog.getIpAddress()) .ipAddressName(loginLog == null ? null : loginLog.getIpAddressName()) .browser(loginLog == null ? null : loginLog.getBrowser()) .platform(loginLog == null ? null : loginLog.getPlatForm()) .requestUrl(event.getRequestUrl()) .requestMethod(event.getRequestMethod()) .build(); } String text = StringUtil.isEmpty(description) ? logoutDescription(reason) : description; LogEntity logoutLog = buildLog(closeEvent, logoutLogId, 1, session.getLoginLogId(), reason, 1, text); logMapper.insert(logoutLog); return logoutLogId; } private LogEntity buildLog(LoginSessionEventModel event, String id, int loginType, String relatedLogId, String reason, int eventOrder, String description) { UserInfo userInfo = event.getUserInfo(); LogEntity log = new LogEntity(); log.setId(id); log.setType(LogSortEnum.LOGIN.getCode()); log.setLoginType(loginType); log.setLoginMark(1); log.setSessionId(event.getSessionId()); log.setRelatedLogId(relatedLogId); log.setLogoutReason(reason); log.setDeviceType(userInfo == null ? null : userInfo.getLoginDevice()); log.setEventTime(eventTime(event)); log.setTransitionId(event.getTransitionId()); log.setEventOrder(eventOrder); log.setUserId(userInfo == null ? null : userInfo.getUserId()); log.setUserName(userName(userInfo)); log.setIpAddress(event.getIpAddress()); log.setIpAddressName(event.getIpAddressName()); log.setBrowser(event.getBrowser()); log.setPlatForm(event.getPlatform()); log.setRequestUrl(event.getRequestUrl()); log.setRequestMethod(event.getRequestMethod()); log.setRequestDuration(0); log.setDescription(description); log.setCreatorTime(eventTime(event)); log.setCreatorUserId(userInfo == null ? null : userInfo.getUserId()); return log; } private void fillUser(LoginSessionEntity session, UserInfo userInfo) { if (userInfo == null) { return; } session.setTenantId(userInfo.getTenantId()); session.setUserId(userInfo.getUserId()); session.setUserName(userName(userInfo)); session.setDeviceType(userInfo.getLoginDevice()); session.setGrantType(userInfo.getGrantType()); } private String userName(UserInfo userInfo) { if (userInfo == null) { return null; } if (StringUtil.isEmpty(userInfo.getUserAccount())) { return userInfo.getUserName(); } return StringUtil.isEmpty(userInfo.getUserName()) ? userInfo.getUserAccount() : userInfo.getUserName() + "/" + userInfo.getUserAccount(); } private Date eventTime(LoginSessionEventModel event) { return event.getEventTime() == null ? DateUtil.getNowDate() : event.getEventTime(); } private Date parseLoginTime(UserInfo userInfo, Date fallback) { if (userInfo == null || StringUtil.isEmpty(userInfo.getLoginTime())) { return fallback; } try { return DateUtil.stringToDate(userInfo.getLoginTime()); } catch (Exception ignored) { return fallback; } } private String logoutDescription(String reason) { if (LogoutReason.MANUAL.equals(reason)) return "主动退出"; if (LogoutReason.REPLACED.equals(reason)) return "登录顶替"; if (LogoutReason.KICKOUT.equals(reason)) return "强制下线"; if (LogoutReason.TIMEOUT.equals(reason)) return "登录超时"; if (LogoutReason.RELOGIN_RECOVERY.equals(reason)) return "再次登录补偿"; return "异常退出补偿"; } }