用户在线心跳
This commit is contained in:
@@ -8,6 +8,7 @@ import com.honeycombis.honeycom.auth.endpoint.service.HoneycomCustomerService;
|
||||
import com.honeycombis.honeycom.common.core.constant.SecurityConstants;
|
||||
import com.honeycombis.honeycom.common.core.util.R;
|
||||
import com.honeycombis.honeycom.common.core.util.SpringContextHolder;
|
||||
import com.honeycombis.honeycom.common.log.annotation.SysLog;
|
||||
import com.honeycombis.honeycom.common.security.annotation.Inner;
|
||||
import com.honeycombis.honeycom.common.security.dto.ObtainTokenDTO;
|
||||
import com.honeycombis.honeycom.common.security.service.HoneyComCidAuthService;
|
||||
@@ -93,6 +94,7 @@ public class HoneycomCustomerController {
|
||||
*/
|
||||
@Inner(value = false)
|
||||
@PostMapping("/v1/logout")
|
||||
@SysLog("退出登录")
|
||||
public R<Boolean> logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
|
||||
if (StrUtil.isBlank(authHeader)) {
|
||||
return R.ok();
|
||||
|
||||
@@ -32,5 +32,5 @@ public class MessageListUserQueryDto {
|
||||
private Integer isRead;
|
||||
|
||||
@Schema(description="消息标题")
|
||||
private Integer msgTitle;
|
||||
private String msgTitle;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
<version>3.2.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.honeycombis.honeycom.common.feign.annotation.EnableHoneycomFeignClien
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* @author honeycom archetype
|
||||
@@ -14,6 +15,7 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
@EnableHoneycomFeignClients
|
||||
@EnableDiscoveryClient
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class HoneycomSpdmApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HoneycomSpdmApplication.class, args);
|
||||
|
||||
@@ -19,14 +19,9 @@
|
||||
|
||||
package com.honeycombis.honeycom.spdm.controller;
|
||||
|
||||
import com.honeycombis.honeycom.common.core.constant.SecurityConstants;
|
||||
import com.honeycombis.honeycom.common.core.util.R;
|
||||
import com.honeycombis.honeycom.msg.api.dto.MessageOpenApiDTO;
|
||||
import com.honeycombis.honeycom.spdm.dto.MessageDto;
|
||||
import com.honeycombis.honeycom.spdm.dto.SysLogDto;
|
||||
import com.honeycombis.honeycom.spdm.feign.RemoteLogServiceFeign;
|
||||
import com.honeycombis.honeycom.spdm.feign.RemoteMsgServiceFeign;
|
||||
import com.honeycombis.honeycom.spdm.util.ResponseR;
|
||||
import com.honeycombis.honeycom.spdm.feign.SpdmServiceFeignClient;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -46,13 +41,14 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class SpdmLogController {
|
||||
|
||||
@Resource
|
||||
private RemoteLogServiceFeign remoteLogServiceFeign;
|
||||
private SpdmServiceFeignClient spdmServiceFeignClient;
|
||||
|
||||
@Operation(summary = "记录日志")
|
||||
@PostMapping(value = "/saveLog")
|
||||
public ResponseR saveLog(@RequestBody SysLogDto messageDto) {
|
||||
R<Boolean> r = remoteLogServiceFeign.saveLog(messageDto, SecurityConstants.FROM_IN);
|
||||
return ResponseR.ok(r.getData());
|
||||
public R<Void> saveLog(@RequestBody SysLogDto sysLog) {
|
||||
// R<Boolean> r = remoteLogServiceFeign.saveLog(messageDto, SecurityConstants.FROM_IN);
|
||||
spdmServiceFeignClient.saveLog(sysLog);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,29 +39,43 @@ import com.honeycombis.honeycom.user.vo.SysUserVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping("/spdm-user")
|
||||
@Tag(description = "spdm", name = "提供给SPDM的用户模块")
|
||||
public class SpdmUserController {
|
||||
|
||||
@Resource
|
||||
private RemoteUserServiceFeign remoteUserServiceFeign;
|
||||
@Resource
|
||||
private RemoteTenantServiceFeign remoteTenantServiceFeign;
|
||||
@Resource
|
||||
private RemoteAuthServiceFeign remoteAuthServiceFeign;
|
||||
// 心跳相关的Redis key前缀
|
||||
private static final String HEARTBEAT_PREFIX = "user:heartbeat:";
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final RemoteUserServiceFeign remoteUserServiceFeign;
|
||||
private final RemoteTenantServiceFeign remoteTenantServiceFeign;
|
||||
private final RemoteAuthServiceFeign remoteAuthServiceFeign;
|
||||
|
||||
// 构造器注入:指定Bean名称为 stringRedisTemplate
|
||||
public SpdmUserController(@Qualifier("stringRedisTemplate") StringRedisTemplate stringRedisTemplate, RemoteUserServiceFeign remoteUserServiceFeign, RemoteTenantServiceFeign remoteTenantServiceFeign, RemoteAuthServiceFeign remoteAuthServiceFeign) {
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
this.remoteUserServiceFeign = remoteUserServiceFeign;
|
||||
this.remoteTenantServiceFeign = remoteTenantServiceFeign;
|
||||
this.remoteAuthServiceFeign = remoteAuthServiceFeign;
|
||||
}
|
||||
|
||||
@Operation(summary = "条件查询用户列表")
|
||||
@PostMapping(value = "/listUser")
|
||||
@@ -187,8 +201,34 @@ public class SpdmUserController {
|
||||
R<TokenDTO> tokenDTOR = remoteAuthServiceFeign.getClientUserToken(userParamDto.getUserId(), tenantId, authHeader);
|
||||
TokenDTO tokenDTO = tokenDTOR.getData();
|
||||
tokenDTO.setCid_user_id(String.valueOf(userParamDto.getUserId()));
|
||||
tokenDTO.setCid_tenant_id(userParamDto.getTenantId());
|
||||
tokenDTO.setCid_tenant_id(String.valueOf(tenantId));
|
||||
return ResponseR.ok(tokenDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收心跳请求
|
||||
* POST /api/user/heartbeat
|
||||
*/
|
||||
@PostMapping("/heartbeat")
|
||||
public R<Void> heartbeat(@RequestBody HeartbeatRequest request, HttpServletRequest httpRequest) {
|
||||
Long userId = request.getUserId();
|
||||
Long tenantId = request.getTenantId();
|
||||
if (userId == null || tenantId == null) {
|
||||
return R.failed("用户ID和租户ID不能为空");
|
||||
}
|
||||
// 更新心跳时间
|
||||
updateUserHeartbeat(userId, tenantId);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户心跳时间
|
||||
*/
|
||||
private void updateUserHeartbeat(Long userId, Long tenantId) {
|
||||
String key = HEARTBEAT_PREFIX + tenantId + ":" + userId;
|
||||
String currentTime = LocalDateTime.now().toString();
|
||||
// 存储心跳时间
|
||||
stringRedisTemplate.opsForValue().set(key, currentTime);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.honeycombis.honeycom.spdm.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class HeartbeatRequest {
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Long userId;
|
||||
|
||||
@NotNull(message = "租户ID不能为空")
|
||||
private Long tenantId;
|
||||
// 可选:前端时间戳
|
||||
private Long timestamp;
|
||||
|
||||
// 可选:其他信息
|
||||
private String pageUrl;
|
||||
private String browserInfo;
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import java.io.Serializable;
|
||||
public class TokenDTO implements Serializable {
|
||||
|
||||
private String access_token;
|
||||
private String refresh_token;
|
||||
private String cid_user_id;
|
||||
private String cid_tenant_id;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.honeycombis.honeycom.spdm.feign;
|
||||
|
||||
import com.honeycombis.honeycom.common.core.util.R;
|
||||
import com.honeycombis.honeycom.spdm.dto.ApproveResultDto;
|
||||
import com.honeycombis.honeycom.spdm.dto.SysLogDto;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -14,4 +15,7 @@ public interface SpdmServiceFeignClient {
|
||||
@PostMapping("/systemApprove/approveStatusNotice")
|
||||
R approveStatusNotice(@RequestBody ApproveResultDto approveResultDto);
|
||||
|
||||
@PostMapping("/systemLog/saveLog")
|
||||
R saveLog(@RequestBody SysLogDto sysLogDto);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.honeycombis.honeycom.spdm.job;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.honeycombis.honeycom.spdm.dto.SysLogDto;
|
||||
import com.honeycombis.honeycom.spdm.feign.*;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class HeartbeatMonitorTask {
|
||||
|
||||
private static final String HEARTBEAT_PREFIX = "user:heartbeat:";
|
||||
private static final long HEARTBEAT_TIMEOUT = 7 * 60 * 1000L; // 前端每5分钟发送一次心跳,设置超时时间7分钟
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final SpdmServiceFeignClient spdmServiceFeignClient;
|
||||
|
||||
public HeartbeatMonitorTask(@Qualifier("stringRedisTemplate")StringRedisTemplate redisTemplate, SpdmServiceFeignClient spdmServiceFeignClient) {
|
||||
this.stringRedisTemplate = redisTemplate;
|
||||
this.spdmServiceFeignClient = spdmServiceFeignClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 每2分钟检查一次心跳
|
||||
*/
|
||||
@Scheduled(fixedDelay = 2 * 60 * 1000L)
|
||||
public void checkHeartbeatTimeout() {
|
||||
Set<String> keys = stringRedisTemplate.keys(HEARTBEAT_PREFIX + "*");
|
||||
|
||||
if (keys != null && !keys.isEmpty()) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
for (String key : keys) {
|
||||
log.info("[HeartbeatMonitorTask] check heartbeat timeout for key: {}", key);
|
||||
Object value = stringRedisTemplate.opsForValue().get(key);
|
||||
if (value != null) {
|
||||
try {
|
||||
LocalDateTime lastHeartbeat = LocalDateTime.parse(value.toString());
|
||||
LocalDateTime timeoutTime = lastHeartbeat.plusSeconds(HEARTBEAT_TIMEOUT);
|
||||
|
||||
// 如果超过心跳超时时间,认为用户已离线
|
||||
if (now.isAfter(timeoutTime)) {
|
||||
// 截取前缀后的部分,再按冒号拆分
|
||||
String suffix = key.substring(HEARTBEAT_PREFIX.length());
|
||||
String[] parts = suffix.split(":", 2);
|
||||
Long tenantId = Long.valueOf(parts[0]);
|
||||
Long userId = Long.valueOf(parts[1]);
|
||||
handleUserOffline(userId, tenantId, lastHeartbeat);
|
||||
// 清除过期的心跳记录
|
||||
stringRedisTemplate.delete(key);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 解析失败,清除无效key
|
||||
stringRedisTemplate.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户离线
|
||||
*/
|
||||
private void handleUserOffline(Long userId, Long tenantId, LocalDateTime lastHeartbeat) {
|
||||
// 记录退出日志
|
||||
SysLogDto sysLog = new SysLogDto();
|
||||
sysLog.setTitle("退出登录");
|
||||
sysLog.setServiceId("simulation-system");
|
||||
sysLog.setTenantId(tenantId);
|
||||
sysLog.setCreateBy(String.valueOf(userId));
|
||||
log.info("[HeartbeatMonitorTask] sysLog param:{}", JSONUtil.toJsonStr(sysLog));
|
||||
spdmServiceFeignClient.saveLog(sysLog);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user