用户在线心跳

This commit is contained in:
2025-12-16 10:47:43 +08:00
parent 2172536ad6
commit 3172d37788
10 changed files with 175 additions and 20 deletions

View File

@@ -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();

View File

@@ -32,5 +32,5 @@ public class MessageListUserQueryDto {
private Integer isRead;
@Schema(description="消息标题")
private Integer msgTitle;
private String msgTitle;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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