From 3f2b07ddd01880e138dbd552f1c00b38c596c326 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Sat, 7 Nov 2026 11:21:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E5=AE=B6=E5=B1=9E=E7=AB=AF?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=88=97=E8=A1=A8=E8=BF=94=E5=9B=9E=E5=9C=A8?= =?UTF-8?q?=E7=BA=BF=E7=8A=B6=E6=80=81=E4=B8=8E=E6=9C=80=E5=90=8E=E5=BF=83?= =?UTF-8?q?=E8=B7=B3=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 批量查询 Redis 心跳状态,避免逐台查询 - 修正心跳时间 Redis 序列化格式,防止读取时解析失败导致状态恒为离线 --- .../client/controller/ClientController.java | 14 ++++++ .../boot/device/model/vo/DeviceBriefVO.java | 6 +++ .../boot/device/model/vo/DeviceOnlineVO.java | 3 ++ .../DeviceOnlineRedisRoundTripTest.java | 45 +++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 src/test/java/com/youlai/boot/device/DeviceOnlineRedisRoundTripTest.java diff --git a/src/main/java/com/youlai/boot/client/controller/ClientController.java b/src/main/java/com/youlai/boot/client/controller/ClientController.java index 179f26e5..1f817748 100644 --- a/src/main/java/com/youlai/boot/client/controller/ClientController.java +++ b/src/main/java/com/youlai/boot/client/controller/ClientController.java @@ -20,12 +20,14 @@ import com.youlai.boot.device.model.vo.DevicePictureVO; import com.youlai.boot.device.model.vo.WallpaperUploadVO; import com.youlai.boot.device.model.vo.DeviceBriefVO; import com.youlai.boot.device.model.vo.DeviceLocationVO; +import com.youlai.boot.device.model.vo.DeviceOnlineVO; import com.youlai.boot.device.model.vo.DevicePolicyVO; import com.youlai.boot.device.model.vo.DeviceSystemInfoVO; import com.youlai.boot.device.model.vo.DeviceWallpaperVO; import com.youlai.boot.device.model.vo.ScreenshotVO; import com.youlai.boot.device.service.ApkInstallService; import com.youlai.boot.device.service.DeviceAlarmService; +import com.youlai.boot.device.service.DeviceOnlineService; import com.youlai.boot.device.service.DevicePictureService; import com.youlai.boot.device.service.DevicePolicyService; import com.youlai.boot.device.service.DeviceService; @@ -58,6 +60,7 @@ import com.youlai.boot.support.file.service.FileService; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * 客户端(家属端)控制层 @@ -85,6 +88,7 @@ public class ClientController { private final DeviceWallpaperService deviceWallpaperService; private final DevicePictureService devicePictureService; private final DeviceAlarmService deviceAlarmService; + private final DeviceOnlineService deviceOnlineService; private final FileService fileService; /** @@ -215,6 +219,13 @@ public class ClientController { .orderByDesc(SnDeviceInfo::getCreateTime) ); + // 批量查询在线状态(Redis 心跳 TTL 判定),避免逐台查询 + List sns = devices.stream() + .map(SnDeviceInfo::getSerialno) + .filter(StringUtils::hasText) + .toList(); + Map onlineMap = deviceOnlineService.getOnlineStatusBySns(sns); + List list = devices.stream().map(d -> { DeviceBriefVO vo = new DeviceBriefVO(); vo.setSerialno(d.getSerialno()); @@ -223,6 +234,9 @@ public class ClientController { vo.setSnMobile(d.getSnMobile()); vo.setStatus(d.getStatus()); vo.setActivateTime(d.getActivateTime()); + DeviceOnlineVO online = onlineMap.get(d.getSerialno()); + vo.setOnline(online != null ? online.getOnline() : 0); + vo.setLastHeartbeatTime(online != null ? online.getLastHeartbeatTime() : null); return vo; }).toList(); diff --git a/src/main/java/com/youlai/boot/device/model/vo/DeviceBriefVO.java b/src/main/java/com/youlai/boot/device/model/vo/DeviceBriefVO.java index b99d4e50..d35ee6e8 100644 --- a/src/main/java/com/youlai/boot/device/model/vo/DeviceBriefVO.java +++ b/src/main/java/com/youlai/boot/device/model/vo/DeviceBriefVO.java @@ -33,4 +33,10 @@ public class DeviceBriefVO implements Serializable { @Schema(description = "激活时间") private LocalDateTime activateTime; + + @Schema(description = "在线状态(1:在线;0:离线)") + private Integer online; + + @Schema(description = "最后心跳时间") + private LocalDateTime lastHeartbeatTime; } diff --git a/src/main/java/com/youlai/boot/device/model/vo/DeviceOnlineVO.java b/src/main/java/com/youlai/boot/device/model/vo/DeviceOnlineVO.java index bc31e138..66f59975 100644 --- a/src/main/java/com/youlai/boot/device/model/vo/DeviceOnlineVO.java +++ b/src/main/java/com/youlai/boot/device/model/vo/DeviceOnlineVO.java @@ -1,5 +1,6 @@ package com.youlai.boot.device.model.vo; +import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -21,6 +22,8 @@ public class DeviceOnlineVO { @Schema(description = "在线状态(1:在线;0:离线)") private Integer online; + // 显式指定格式,否则 Redis 序列化器写入 ISO 带 T/纳秒,全局 ObjectMapper 按空格格式反序列化会失败 + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Schema(description = "最后心跳时间") private LocalDateTime lastHeartbeatTime; diff --git a/src/test/java/com/youlai/boot/device/DeviceOnlineRedisRoundTripTest.java b/src/test/java/com/youlai/boot/device/DeviceOnlineRedisRoundTripTest.java new file mode 100644 index 00000000..f42f50ee --- /dev/null +++ b/src/test/java/com/youlai/boot/device/DeviceOnlineRedisRoundTripTest.java @@ -0,0 +1,45 @@ +package com.youlai.boot.device; + +import com.youlai.boot.config.JacksonConfig; +import com.youlai.boot.device.model.vo.DeviceOnlineVO; +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.json.JsonMapper; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * 设备在线状态在 Redis 写入与全局 ObjectMapper 读取之间的格式一致性校验 + *

+ * RedisConfig 的序列化器与 JacksonConfig 的全局时间格式不一致时, + * convertValue 会抛 DateTimeParseException,导致在线状态永远读成离线。 + */ +class DeviceOnlineRedisRoundTripTest { + + @Test + void redisValueCanBeConvertedByGlobalObjectMapper() { + JsonMapper redisMapper = JsonMapper.builder() + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + JacksonJsonRedisSerializer serializer = new JacksonJsonRedisSerializer<>(redisMapper, Object.class); + + LocalDateTime heartbeat = LocalDateTime.of(2026, 11, 7, 10, 37, 22); + + DeviceOnlineVO vo = new DeviceOnlineVO(); + vo.setSerialno("895Y044WC"); + vo.setOnline(1); + vo.setIp("192.168.100.141"); + vo.setScreenState(0); + vo.setLastHeartbeatTime(heartbeat); + + Object cached = serializer.deserialize(serializer.serialize(vo)); + DeviceOnlineVO converted = new JacksonConfig().objectMapper().convertValue(cached, DeviceOnlineVO.class); + + assertEquals("895Y044WC", converted.getSerialno()); + assertEquals(1, converted.getOnline()); + assertEquals(heartbeat, converted.getLastHeartbeatTime()); + } +}