diff --git a/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java b/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java index 2cbe4a745..e848d38f4 100644 --- a/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java +++ b/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java @@ -19,8 +19,8 @@ public class VideoManagerConstants { public static final String ONLINE_MEDIA_SERVERS_PREFIX = "VMP_ONLINE_MEDIA_SERVERS:"; public static final String DEVICE_PREFIX = "VMP_DEVICE_INFO"; - public static final String DEVICE_KEEPALIVE_PREFIX = "DEVICE_KEEPALIVE:"; - public static final String DEVICE_REGISTER_PREFIX = "DEVICE_REGISTER:"; + public static final String DEVICE_KEEPALIVE_PREFIX = "VMP_DEVICE_KEEPALIVE:"; + public static final String DEVICE_REGISTER_PREFIX = "VMP_DEVICE_REGISTER:"; public static final String INVITE_PREFIX = "VMP_GB_INVITE_INFO"; diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java index f36a4d555..3bfee31ba 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java @@ -6,6 +6,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericToStringSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @@ -43,4 +44,22 @@ public class RedisTemplateConfig { return redisTemplate; } + @Bean + public RedisTemplate redisLongTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + // Key 使用 String 序列化 + template.setKeySerializer(new StringRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + + // Value 使用 GenericToStringSerializer,它能将 Long 转换为纯文本字符串存入 Redis + // 这样在 Redis 命令行输入 'get key' 看到的是 "123" 而不是二进制乱码 + template.setValueSerializer(new GenericToStringSerializer<>(Long.class)); + template.setHashValueSerializer(new GenericToStringSerializer<>(Long.class)); + + template.afterPropertiesSet(); + return template; + } + } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java index 75d52a624..83f1a6b3c 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java @@ -89,15 +89,15 @@ public class Device { /** * 注册时间 */ - @Schema(description = "注册时间") - private String registerTime; + @Schema(description = "注册时间戳") + private Long registerTimeStamp; /** * 心跳时间 */ @Schema(description = "心跳时间") - private String keepaliveTime; + private Long keepaliveTimeStamp; /** diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/TimeStatistics.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/TimeStatistics.java new file mode 100644 index 000000000..975f858ec --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/TimeStatistics.java @@ -0,0 +1,20 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@Schema(description = "时间统计信息") +public class TimeStatistics { + + @Schema(description = "时间") + private String time; + + @Schema(description = "时间差") + private Long timeDiff; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceQuery.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceQuery.java index a4c3bf2da..b2d12240c 100755 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceQuery.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceQuery.java @@ -8,6 +8,7 @@ import com.genersoft.iot.vmp.conf.security.JwtUtils; import com.genersoft.iot.vmp.gb28181.bean.Device; import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; import com.genersoft.iot.vmp.gb28181.bean.SyncStatus; +import com.genersoft.iot.vmp.gb28181.bean.TimeStatistics; import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; import com.genersoft.iot.vmp.gb28181.service.IDeviceService; import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; @@ -21,8 +22,10 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.io.IOUtils; import org.apache.ibatis.annotations.Options; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; @@ -31,12 +34,11 @@ import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.async.DeferredResult; -import jakarta.servlet.ServletOutputStream; -import jakarta.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; +import java.util.List; @Tag(name = "国标设备查询", description = "国标设备查询") @SuppressWarnings("rawtypes") @@ -136,7 +138,7 @@ public class DeviceQuery { log.debug("设备通道信息同步API调用,deviceId:" + deviceId); } Device device = deviceService.getDeviceByDeviceId(deviceId); - if (device.getRegisterTime() == null) { + if (device.getTransport() == null) { WVPResult wvpResult = new WVPResult<>(); wvpResult.setCode(ErrorCode.ERROR100.getCode()); wvpResult.setMsg("设备尚未注册过"); @@ -155,7 +157,7 @@ public class DeviceQuery { log.debug("设备信息删除API调用,deviceId:" + deviceId); } - // 清除redis记录 + // 清除 redis 记录 deviceService.delete(deviceId); JSONObject json = new JSONObject(); json.put("deviceId", deviceId); @@ -181,8 +183,7 @@ public class DeviceQuery { DeviceChannel deviceChannel = deviceChannelService.getOne(deviceId,channelId); if (deviceChannel == null) { - PageInfo deviceChannelPageResult = new PageInfo<>(); - return deviceChannelPageResult; + return new PageInfo<>(); } return deviceChannelService.getSubChannels(deviceChannel.getDataDeviceId(), channelId, query, channelType, online, page, count); @@ -430,4 +431,28 @@ public class DeviceQuery { public void subscribeMobilePosition(int id, int cycle, int interval) { deviceService.subscribeMobilePosition(id, cycle, interval); } + + @GetMapping("/statistics/keepalive") + @Operation(summary = "请求心跳统计") + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "count", description = "返回的数量,按时间正向排序,返回的最新的", required = true) + public List getKeepaliveTimeStatistics(String deviceId, Integer count) { + if (ObjectUtils.isEmpty(deviceId)) { + return List.of(); + } + return deviceService.getKeepaliveTimeStatistics(deviceId, count); + } + + @GetMapping("/statistics/register") + @Operation(summary = "请求注册统计") + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "count", description = "返回的数量,按时间正向排序,返回的最新的", required = true) + public List getRegisterTimeStatistics(String deviceId, Integer count) { + if (ObjectUtils.isEmpty(deviceId)) { + return List.of(); + } + return deviceService.getRegisterTimeStatistics(deviceId, count); + } + + } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java index 7e60719cb..3ce9e825e 100755 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java @@ -90,13 +90,6 @@ public interface IDeviceService { List getAllByStatus(Boolean status); - /** - * 判断是否注册已经失效 - * @param device 设备信息 - * @return 布尔 - */ - boolean expire(Device device); - /** * 检查设备状态 * @param device 设备信息 @@ -201,4 +194,7 @@ public interface IDeviceService { void queryPreset(Device device, String channelId, ErrorCallback> callback); + List getKeepaliveTimeStatistics(String deviceId, Integer count); + + List getRegisterTimeStatistics(String deviceId, Integer count); } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java index 7991be477..c94d0ceb0 100755 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java @@ -54,7 +54,6 @@ import javax.sip.InvalidArgumentException; import javax.sip.ResponseEvent; import javax.sip.SipException; import java.text.ParseException; -import java.time.Instant; import java.util.*; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; @@ -677,13 +676,6 @@ public class DeviceServiceImpl implements IDeviceService, CommandLineRunner { return deviceMapper.getDevices(ChannelDataType.GB28181, status); } - @Override - public boolean expire(Device device) { - Instant registerTimeDate = Instant.from(DateUtil.formatter.parse(device.getRegisterTime())); - Instant expireInstant = registerTimeDate.plusMillis(TimeUnit.SECONDS.toMillis(device.getExpires())); - return expireInstant.isBefore(Instant.now()); - } - @Override public Boolean getDeviceStatus(@NotNull Device device) { SynchronousQueue queue = new SynchronousQueue<>(); @@ -1258,5 +1250,36 @@ public class DeviceServiceImpl implements IDeviceService, CommandLineRunner { } } + @Override + public List getKeepaliveTimeStatistics(String deviceId, Integer count) { + List timeStampList = redisCatchStorage.getDeviceKeepaliveTimeStamp(deviceId, count); + return formateTimeStatistics(timeStampList, count); + } + @Override + public List getRegisterTimeStatistics(String deviceId, Integer count) { + List timeStampList = redisCatchStorage.getDeviceRegisterTimeStamp(deviceId, count); + return formateTimeStatistics(timeStampList, count); + } + + private List formateTimeStatistics(List timeStampList, Integer count) { + if (timeStampList.isEmpty()) { + return List.of(); + } + List timeStatisticsList = new ArrayList<>(); + for (int i = 0; i < timeStampList.size(); i++) { + Long timeStamp = timeStampList.get(i); + TimeStatistics timeStatistics = new TimeStatistics(); + timeStatistics.setTime(DateUtil.timestampMsTo_yyyy_MM_dd_HH_mm_ss(timeStamp)); + if (i > 0) { + Long lastTimeStamp = timeStampList.get(i - 1); + timeStatistics.setTimeDiff((timeStamp - lastTimeStamp) / 1000); + } + timeStatisticsList.add(timeStatistics); + } + if (timeStatisticsList.size() > count) { + timeStatisticsList = timeStatisticsList.subList(timeStatisticsList.size() - count, timeStatisticsList.size()); + } + return timeStatisticsList; + } } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java index db6615aeb..73d5459ed 100755 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java @@ -1,11 +1,11 @@ package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; +import com.genersoft.iot.vmp.common.RemoteAddressInfo; import com.genersoft.iot.vmp.conf.SipConfig; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.gb28181.auth.DigestServerAuthenticationHelper; import com.genersoft.iot.vmp.gb28181.bean.Device; import com.genersoft.iot.vmp.gb28181.bean.GbSipDate; -import com.genersoft.iot.vmp.common.RemoteAddressInfo; import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; import com.genersoft.iot.vmp.gb28181.service.IDeviceService; import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; @@ -13,7 +13,7 @@ import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; import com.genersoft.iot.vmp.gb28181.utils.SipUtils; -import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; import com.genersoft.iot.vmp.utils.IpPortUtil; import gov.nist.javax.sip.address.AddressImpl; import gov.nist.javax.sip.address.SipUri; @@ -23,7 +23,6 @@ import gov.nist.javax.sip.message.SIPResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; @@ -38,10 +37,8 @@ import javax.sip.message.Response; import java.security.NoSuchAlgorithmException; import java.text.ParseException; import java.util.Calendar; +import java.util.List; import java.util.Locale; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; /** * SIP命令类型: REGISTER请求 @@ -67,6 +64,9 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen @Autowired private UserSetting userSetting; + @Autowired + private IRedisCatchStorage redisCatchStorage; + @Override public void afterPropertiesSet() throws Exception { @@ -119,7 +119,7 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen String transport = reqViaHeader.getTransport(); device.setTransport("TCP".equalsIgnoreCase(transport) ? "TCP" : "UDP"); sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), registerOkResponse); - device.setRegisterTime(DateUtil.getNow()); + device.setRegisterTimeStamp(System.currentTimeMillis()); deviceService.online(device); } else { deviceService.offline(device); @@ -218,7 +218,7 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen // 注册成功 device.setExpires(request.getExpires().getExpires()); registerFlag = true; - // 判断TCP还是UDP + // 判断 TCP/UDP ViaHeader reqViaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); String transport = reqViaHeader.getTransport(); device.setTransport("TCP".equalsIgnoreCase(transport) ? "TCP" : "UDP"); @@ -226,10 +226,10 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); // 注册成功 - // 保存到redis + device.setRegisterTimeStamp(System.currentTimeMillis()); + // 保存到 redis if (registerFlag) { log.info("[注册成功] deviceId: {}->{}", deviceId, requestAddress); - device.setRegisterTime(DateUtil.getNow()); SipTransactionInfo sipTransactionInfo = new SipTransactionInfo((SIPResponse) response); device.setSipTransactionInfo(sipTransactionInfo); deviceService.online(device); @@ -237,6 +237,7 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen log.info("[注销成功] deviceId: {}->{}", deviceId, requestAddress); deviceService.offline(device); } + redisCatchStorage.updateDeviceRegisterTimeStamp(List.of(device)); } catch (SipException | NoSuchAlgorithmException | ParseException e) { log.error("未处理的异常 ", e); } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java index 336f4dd34..1493bd9bd 100755 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java @@ -10,14 +10,13 @@ import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorP import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; import com.genersoft.iot.vmp.gb28181.utils.SipUtils; -import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; import com.genersoft.iot.vmp.utils.IpPortUtil; import gov.nist.javax.sip.message.SIPRequest; import lombok.extern.slf4j.Slf4j; import org.dom4j.Element; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -26,7 +25,6 @@ import javax.sip.RequestEvent; import javax.sip.SipException; import javax.sip.message.Response; import java.text.ParseException; -import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -55,6 +53,9 @@ public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent imp @Autowired private UserSetting userSetting; + @Autowired + private IRedisCatchStorage redisCatchStorage; + @Override public void afterPropertiesSet() throws Exception { notifyMessageHandler.addHandler(cmdType, this); @@ -78,8 +79,7 @@ public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent imp device.setIp(remoteAddressInfo.getIp()); device.setLocalIp(request.getLocalAddress().getHostAddress()); } - - + device.setKeepaliveTimeStamp(System.currentTimeMillis()); if (device.isOnLine()) { taskQueue.add(device); long expiresTime = Math.min(device.getExpires(), device.getHeartBeatInterval() * device.getHeartBeatCount()) * 1000L; @@ -94,6 +94,7 @@ public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent imp @Scheduled(fixedDelay = 10, timeUnit = TimeUnit.SECONDS) public void executeUpdateDeviceList() { if (!taskQueue.isEmpty()) { + redisCatchStorage.updateDeviceKeepaliveTimeStamp(taskQueue.stream().toList()); taskQueue.clear(); } } diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDeviceController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDeviceController.java index ee6eec485..c4491905a 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDeviceController.java +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDeviceController.java @@ -62,7 +62,7 @@ public class RedisRpcDeviceController extends RpcController { response.setBody("param error"); return response; } - if (device.getRegisterTime() == null) { + if (device.getTransport() == null) { response.setStatusCode(ErrorCode.ERROR400.getCode()); response.setBody("设备尚未注册过"); return response; diff --git a/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java b/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java index 7e30674de..d3548d6e5 100755 --- a/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java @@ -184,8 +184,11 @@ public interface IRedisCatchStorage { String chooseOneServer(String serverId); - void updateDeviceKeepaliveTime(List deviceList); + void updateDeviceKeepaliveTimeStamp(List deviceList); - void updateDeviceRegisterTime(List deviceList); + List getDeviceKeepaliveTimeStamp(String deviceId, Integer count); + void updateDeviceRegisterTimeStamp(List deviceList); + + List getDeviceRegisterTimeStamp(String deviceId, Integer count); } diff --git a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java index 5ce4ae17f..0e100c529 100755 --- a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java @@ -2,7 +2,6 @@ package com.genersoft.iot.vmp.storager.impl; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; -import com.fasterxml.jackson.core.JsonProcessingException; import com.genersoft.iot.vmp.common.ServerInfo; import com.genersoft.iot.vmp.common.SystemAllInfo; import com.genersoft.iot.vmp.common.VideoManagerConstants; @@ -21,7 +20,9 @@ import com.genersoft.iot.vmp.utils.JsonUtil; import com.genersoft.iot.vmp.utils.SystemInfoUtils; import com.genersoft.iot.vmp.utils.redis.RedisUtil; import lombok.extern.slf4j.Slf4j; +import org.jspecify.annotations.NonNull; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.SessionCallback; import org.springframework.data.redis.core.StringRedisTemplate; @@ -48,6 +49,9 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage { @Autowired private RedisTemplate redisTemplate; + @Autowired + private RedisTemplate longRedisTemplate; + @Autowired private StringRedisTemplate stringRedisTemplate; @@ -546,25 +550,71 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage { } @Override - public void updateDeviceKeepaliveTime(List deviceList) { -// if (deviceList == null || deviceList.isEmpty()) { -// return; -// } -// // 使用 SessionCallback 保证批量操作在同一个连接中执行 -// SessionCallback sessionCallback = session -> { -// // 1. 批量添加心跳数据到列表尾部 -// for (Device device : deviceList) { -// session.opsForList().rightPush(VideoManagerConstants.DEVICE_KEEPALIVE_PREFIX + device.getDeviceId(), device); -// } -// // 2. 截取列表,只保留最新 100 条 -// session.opsForList().trim(VideoManagerConstants.DEVICE_KEEPALIVE_PREFIX, -1000, -1); -// return true; -// }; -// redisTemplate.execute(sessionCallback); + public void updateDeviceKeepaliveTimeStamp(List deviceList) { + if (deviceList == null || deviceList.isEmpty()) { + return; + } + // 使用 SessionCallback 保证批量操作在同一个连接中执行 + SessionCallback sessionCallback = new SessionCallback<>() { + @Override + // 注意:这里直接写死 String, String 覆盖接口的 K, V + public Boolean execute(@NonNull RedisOperations operations) { + // 1. 批量添加心跳数据到列表尾部 + for (Device device : deviceList) { + operations.opsForList().rightPush(VideoManagerConstants.DEVICE_KEEPALIVE_PREFIX + device.getDeviceId(), device.getKeepaliveTimeStamp()); + // 2. 截取列表,只保留最新 100 条 + operations.opsForList().trim((VideoManagerConstants.DEVICE_KEEPALIVE_PREFIX + device.getDeviceId()), -1000, -1); + } + return true; + } + }; + + longRedisTemplate.execute(sessionCallback); } @Override - public void updateDeviceRegisterTime(List deviceList) { + public List getDeviceKeepaliveTimeStamp(String deviceId, Integer count) { + if (deviceId == null ) { + return List.of(); + } + if (count == null) { + count = 20; + } + return longRedisTemplate.opsForList().range(VideoManagerConstants.DEVICE_KEEPALIVE_PREFIX + deviceId, 0, count + 1); + } + + + @Override + public void updateDeviceRegisterTimeStamp(List deviceList) { + if (deviceList == null || deviceList.isEmpty()) { + return; + } + // 使用 SessionCallback 保证批量操作在同一个连接中执行 + SessionCallback sessionCallback = new SessionCallback<>() { + @Override + // 注意:这里直接写死 String, String 覆盖接口的 K, V + public Boolean execute(@NonNull RedisOperations operations) { + // 1. 批量添加心跳数据到列表尾部 + for (Device device : deviceList) { + operations.opsForList().rightPush(VideoManagerConstants.DEVICE_REGISTER_PREFIX + device.getDeviceId(), device.getRegisterTimeStamp()); + // 2. 截取列表,只保留最新 100 条 + operations.opsForList().trim((VideoManagerConstants.DEVICE_REGISTER_PREFIX + device.getDeviceId()), -1000, -1); + } + return true; + } + }; + longRedisTemplate.execute(sessionCallback); + } + + @Override + public List getDeviceRegisterTimeStamp(String deviceId, Integer count) { + if (deviceId == null ) { + return List.of(); + } + if (count == null) { + count = 20; + } + return longRedisTemplate.opsForList().range(VideoManagerConstants.DEVICE_REGISTER_PREFIX + deviceId, 0, count + 1); } } diff --git a/web/src/api/device.js b/web/src/api/device.js index 95c4f6619..67b8badd5 100644 --- a/web/src/api/device.js +++ b/web/src/api/device.js @@ -243,4 +243,24 @@ export function queryDeviceTree(params, deviceId) { } }) } +export function getKeepaliveTimeStatistics({ deviceId, count }) { + return request({ + method: 'get', + url: '/api/device/query/statistics/keepalive', + params: { + deviceId: deviceId, + count: count + } + }) +} +export function getRegisterTimeStatistics({ deviceId, count }) { + return request({ + method: 'get', + url: '/api/device/query/statistics/register', + params: { + deviceId: deviceId, + count: count + } + }) +} diff --git a/web/src/store/modules/device.js b/web/src/store/modules/device.js index d9265249a..447ef4e69 100644 --- a/web/src/store/modules/device.js +++ b/web/src/store/modules/device.js @@ -2,7 +2,7 @@ import { add, changeChannelAudio, deleteDevice, - deviceRecord, + deviceRecord, getKeepaliveTimeStatistics, getRegisterTimeStatistics, queryBasicParam, queryChannelOne, queryChannels, @@ -242,6 +242,26 @@ const actions = { reject(error) }) }) + }, + getKeepaliveTimeStatistics({ commit }, params) { + return new Promise((resolve, reject) => { + getKeepaliveTimeStatistics(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) + }, + getRegisterTimeStatistics({ commit }, params) { + return new Promise((resolve, reject) => { + getRegisterTimeStatistics(params).then(response => { + const { data } = response + resolve(data) + }).catch(error => { + reject(error) + }) + }) } } diff --git a/web/src/styles/iconfont.scss b/web/src/styles/iconfont.scss index 5f661da1e..05d191e8f 100644 --- a/web/src/styles/iconfont.scss +++ b/web/src/styles/iconfont.scss @@ -1,6 +1,6 @@ @font-face { font-family: "iconfont"; /* Project id 1291092 */ - src: url('iconfont.woff2?t=1758784486763') format('woff2') + src: url('iconfont.woff2?t=1769409737891') format('woff2') } .iconfont { @@ -11,6 +11,18 @@ -moz-osx-font-smoothing: grayscale; } +.icon-xintiao:before { + content: "\e7f4"; +} + +.icon-register:before { + content: "\e7f5"; +} + +.icon-tongji-Statistics:before { + content: "\e7f3"; +} + .icon-mti-duobianxingxuan:before { content: "\e9e7"; } diff --git a/web/src/styles/iconfont.woff2 b/web/src/styles/iconfont.woff2 index a64c51707..3faf60053 100644 Binary files a/web/src/styles/iconfont.woff2 and b/web/src/styles/iconfont.woff2 differ diff --git a/web/src/styles/index.scss b/web/src/styles/index.scss index d58f2d344..40ad229e4 100644 --- a/web/src/styles/index.scss +++ b/web/src/styles/index.scss @@ -64,3 +64,12 @@ div:focus { .app-container { padding: 20px; } + +.iconfont-14 { + font-family: "iconfont" !important; + font-size: 14px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + margin-right: 5px; +} diff --git a/web/src/views/device/list.vue b/web/src/views/device/list.vue index 2a3793e1f..ac27d53b1 100755 --- a/web/src/views/device/list.vue +++ b/web/src/views/device/list.vue @@ -106,6 +106,28 @@ + + + @@ -170,6 +193,7 @@ import deviceEdit from './edit.vue' import syncChannelProgress from '../dialog/SyncChannelProgress.vue' import configInfo from '../dialog/configInfo.vue' +import timeStatistics from './timeStatistics.vue' import Vue from 'vue' export default { @@ -177,7 +201,8 @@ export default { components: { configInfo, deviceEdit, - syncChannelProgress + syncChannelProgress, + timeStatistics }, data() { return { @@ -440,6 +465,12 @@ export default { message: error.message }) }) + }, + getKeepaliveTimeStatistics: function(deviceId) { + this.$refs.timeStatistics.openDialog('心跳时间统计', 'device/getKeepaliveTimeStatistics', deviceId, 10) + }, + getRegisterTimeStatistics: function(deviceId) { + this.$refs.timeStatistics.openDialog('注册时间统计', 'device/getRegisterTimeStatistics', deviceId, 10) } } } diff --git a/web/src/views/device/timeStatistics.vue b/web/src/views/device/timeStatistics.vue new file mode 100644 index 000000000..8d2c1d863 --- /dev/null +++ b/web/src/views/device/timeStatistics.vue @@ -0,0 +1,158 @@ + + +