支持心跳和注册统计

This commit is contained in:
lin 2026-01-26 15:55:07 +08:00
parent 31549bce09
commit a33e7949a4
19 changed files with 453 additions and 65 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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