mirror of
https://gitee.com/pan648540858/wvp-GB28181-pro.git
synced 2026-05-20 04:17:50 +08:00
支持心跳和注册统计
This commit is contained in:
parent
31549bce09
commit
a33e7949a4
@ -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";
|
||||
|
||||
|
||||
@ -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<String, Long> redisLongTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Long> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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<SyncStatus> 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<DeviceChannel> 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<TimeStatistics> 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<TimeStatistics> getRegisterTimeStatistics(String deviceId, Integer count) {
|
||||
if (ObjectUtils.isEmpty(deviceId)) {
|
||||
return List.of();
|
||||
}
|
||||
return deviceService.getRegisterTimeStatistics(deviceId, count);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -90,13 +90,6 @@ public interface IDeviceService {
|
||||
|
||||
List<Device> 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<List<Preset>> callback);
|
||||
|
||||
List<TimeStatistics> getKeepaliveTimeStatistics(String deviceId, Integer count);
|
||||
|
||||
List<TimeStatistics> getRegisterTimeStatistics(String deviceId, Integer count);
|
||||
}
|
||||
|
||||
@ -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<String> queue = new SynchronousQueue<>();
|
||||
@ -1258,5 +1250,36 @@ public class DeviceServiceImpl implements IDeviceService, CommandLineRunner {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TimeStatistics> getKeepaliveTimeStatistics(String deviceId, Integer count) {
|
||||
List<Long> timeStampList = redisCatchStorage.getDeviceKeepaliveTimeStamp(deviceId, count);
|
||||
return formateTimeStatistics(timeStampList, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TimeStatistics> getRegisterTimeStatistics(String deviceId, Integer count) {
|
||||
List<Long> timeStampList = redisCatchStorage.getDeviceRegisterTimeStamp(deviceId, count);
|
||||
return formateTimeStatistics(timeStampList, count);
|
||||
}
|
||||
|
||||
private List<TimeStatistics> formateTimeStatistics(List<Long> timeStampList, Integer count) {
|
||||
if (timeStampList.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<TimeStatistics> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -184,8 +184,11 @@ public interface IRedisCatchStorage {
|
||||
|
||||
String chooseOneServer(String serverId);
|
||||
|
||||
void updateDeviceKeepaliveTime(List<Device> deviceList);
|
||||
void updateDeviceKeepaliveTimeStamp(List<Device> deviceList);
|
||||
|
||||
void updateDeviceRegisterTime(List<Device> deviceList);
|
||||
List<Long> getDeviceKeepaliveTimeStamp(String deviceId, Integer count);
|
||||
|
||||
void updateDeviceRegisterTimeStamp(List<Device> deviceList);
|
||||
|
||||
List<Long> getDeviceRegisterTimeStamp(String deviceId, Integer count);
|
||||
}
|
||||
|
||||
@ -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<Object, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Long> longRedisTemplate;
|
||||
|
||||
@Autowired
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@ -546,25 +550,71 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDeviceKeepaliveTime(List<Device> deviceList) {
|
||||
// if (deviceList == null || deviceList.isEmpty()) {
|
||||
// return;
|
||||
// }
|
||||
// // 使用 SessionCallback 保证批量操作在同一个连接中执行
|
||||
// SessionCallback<Boolean> 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<Device> deviceList) {
|
||||
if (deviceList == null || deviceList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 使用 SessionCallback 保证批量操作在同一个连接中执行
|
||||
SessionCallback<Boolean> 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<Device> deviceList) {
|
||||
public List<Long> 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<Device> deviceList) {
|
||||
if (deviceList == null || deviceList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 使用 SessionCallback 保证批量操作在同一个连接中执行
|
||||
SessionCallback<Boolean> 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<Long> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -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;
|
||||
}
|
||||
|
||||
@ -106,6 +106,28 @@
|
||||
<!-- <el-checkbox label="报警" disabled :checked="scope.row.subscribeCycleForAlarm > 0"></el-checkbox>-->
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="统计" min-width="160">
|
||||
<template v-slot:default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
size="mini"
|
||||
:disabled="scope.row.online===0"
|
||||
icon="iconfont-14 icon-xintiao"
|
||||
title="心跳时间统计"
|
||||
@click="getKeepaliveTimeStatistics(scope.row.deviceId)"
|
||||
>心跳
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
size="mini"
|
||||
:disabled="scope.row.online===0"
|
||||
icon="iconfont-14 icon-register"
|
||||
title="注册时间统计"
|
||||
@click="getRegisterTimeStatistics(scope.row.deviceId)"
|
||||
>注册
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="300" fixed="right">
|
||||
<template v-slot:default="scope">
|
||||
<el-button
|
||||
@ -163,6 +185,7 @@
|
||||
<deviceEdit ref="deviceEdit" />
|
||||
<syncChannelProgress ref="syncChannelProgress" />
|
||||
<configInfo ref="configInfo" />
|
||||
<timeStatistics ref="timeStatistics" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
158
web/src/views/device/timeStatistics.vue
Normal file
158
web/src/views/device/timeStatistics.vue
Normal file
@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div id="timeStatistics" v-loading="loading">
|
||||
<el-dialog
|
||||
v-el-drag-dialog
|
||||
:title="title"
|
||||
width="60%"
|
||||
top="2rem"
|
||||
:close-on-click-modal="false"
|
||||
:visible.sync="showDialog"
|
||||
:destroy-on-close="true"
|
||||
@close="close"
|
||||
>
|
||||
<div style="margin-right: 20px;">
|
||||
<el-row type="flex" justify="space-between" align="middle" style="margin-bottom: 12px;">
|
||||
<div>
|
||||
<el-button-group>
|
||||
<el-button type="primary" :plain="viewMode !== 'table'" size="mini" @click="viewMode = 'table'">表格</el-button>
|
||||
<el-button type="primary" :plain="viewMode !== 'chart'" size="mini" @click="viewMode = 'chart'">折线图</el-button>
|
||||
</el-button-group>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="fetchData" style="margin-left: 8px;">刷新</el-button>
|
||||
</div>
|
||||
<el-form :inline="true" size="mini">
|
||||
<el-form-item label="数量">
|
||||
<el-input-number v-model="count" :min="1" :max="500" @change="fetchData" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
|
||||
<el-table
|
||||
v-if="viewMode === 'table'"
|
||||
:data="list"
|
||||
border
|
||||
size="mini"
|
||||
height="400px"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-table-column prop="time" label="时间" min-width="180" />
|
||||
<el-table-column prop="timeDiff" label="间隔(秒)" min-width="120" />
|
||||
</el-table>
|
||||
|
||||
<ve-line
|
||||
v-else
|
||||
:data="chartData"
|
||||
:extend="extend"
|
||||
height="400px"
|
||||
:legend-visible="false"
|
||||
/>
|
||||
</div>
|
||||
<div style="margin-top: 12px; text-align: right;">
|
||||
<span>最大波动:{{ timeDiffDelta }} 秒</span>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment/moment'
|
||||
import veLine from 'v-charts/lib/line'
|
||||
import request from '@/utils/request'
|
||||
import elDragDialog from '@/directive/el-drag-dialog'
|
||||
|
||||
export default {
|
||||
name: 'TimeStatistics',
|
||||
components: { veLine },
|
||||
directives: { elDragDialog },
|
||||
data() {
|
||||
return {
|
||||
title: null,
|
||||
url: null,
|
||||
deviceId: null,
|
||||
count: 50,
|
||||
showDialog: false,
|
||||
loading: false,
|
||||
viewMode: 'table',
|
||||
list: [],
|
||||
extend: {
|
||||
grid: { right: '30px', containLabel: true },
|
||||
xAxis: {
|
||||
boundaryGap: false,
|
||||
axisLabel: {
|
||||
formatter: (v) => moment(v).format('HH:mm:ss')
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
splitNumber: 6,
|
||||
axisLabel: { formatter: (v) => `${v} 秒` }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (data) => {
|
||||
if (!data || !data.length) return ''
|
||||
const [item] = data
|
||||
return `${moment(item.data[0]).format('HH:mm:ss')}<br/>间隔:${item.data[1]} 秒`
|
||||
}
|
||||
},
|
||||
series: {
|
||||
itemStyle: { color: '#409EFF' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
chartData() {
|
||||
return {
|
||||
columns: ['time', 'timeDiff'],
|
||||
rows: this.list
|
||||
}
|
||||
},
|
||||
timeDiffDelta() {
|
||||
if (!this.list.length) return 0
|
||||
const nums = this.list
|
||||
.map(item => Number(item.timeDiff))
|
||||
.filter(v => !Number.isNaN(v))
|
||||
if (!nums.length) return 0
|
||||
const max = Math.max(...nums)
|
||||
const min = Math.min(...nums)
|
||||
return (max - min).toFixed(2)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openDialog(title, url, deviceId, count = 50) {
|
||||
this.title = title
|
||||
this.url = url
|
||||
this.deviceId = deviceId
|
||||
this.count = count
|
||||
this.showDialog = true
|
||||
this.viewMode = 'table'
|
||||
this.fetchData()
|
||||
},
|
||||
fetchData() {
|
||||
console.log(this.url)
|
||||
if (!this.url || !this.deviceId) return
|
||||
this.loading = true
|
||||
this.$store.dispatch(this.url, {
|
||||
deviceId: this.deviceId,
|
||||
count: this.count
|
||||
}).then(data => {
|
||||
this.list = data
|
||||
}).catch((error) => {
|
||||
this.$message.error({
|
||||
showClose: true,
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.title = null
|
||||
this.url = null
|
||||
this.deviceId = null
|
||||
this.list = []
|
||||
this.showDialog = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Loading…
Reference in New Issue
Block a user