feat(device): 实现设备视频连续性检查功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good

- 新增设备视频连续性检查控制器 DeviceVideoContinuityController
- 提供查询、手动触发和删除检查结果的 REST 接口
- 实现视频连续性检查核心逻辑,支持检测视频间隙
- 添加定时任务 DeviceVideoContinuityCheckTask 自动检查设备视频连续性
- 仅在生产环境(prod)启用,每天9点到18点间每5分钟执行一次
- 支持阿里云OSS和本地存储的视频连续性检查
- 检查结果缓存至 Redis,默认保留24小时
- 新增相关实体类: DeviceVideoContinuityCache、VideoContinuityGap、VideoContinuityResult
- 在存储操作接口中增加 checkVideoContinuity 和 checkRecentVideoContinuity 方法
- 为不支持的存储类型提供默认不支持连续性检查的实现
This commit is contained in:
2025-11-24 14:02:53 +08:00
parent 9278d4479f
commit 4360ef1313
10 changed files with 1104 additions and 2 deletions

View File

@@ -0,0 +1,216 @@
package com.ycwl.basic.task;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.device.DeviceFactory;
import com.ycwl.basic.device.entity.common.DeviceVideoContinuityCache;
import com.ycwl.basic.device.entity.common.VideoContinuityResult;
import com.ycwl.basic.device.operator.IDeviceStorageOperator;
import com.ycwl.basic.integration.common.response.PageResponse;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.repository.DeviceRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* 设备视频连续性检查定时任务
* - 仅在生产环境(prod)运行
* - 每5分钟执行一次
* - 检查前12分钟到前2分钟的视频连续性
* - 仅在9点到18点之间检查
* - 结果缓存到Redis中
*
* @author Claude Code
* @date 2025-09-01
*/
@Slf4j
@Component
@EnableScheduling
@Profile("prod")
public class DeviceVideoContinuityCheckTask {
private static final String REDIS_KEY_PREFIX = "device:video:continuity:";
private static final int CACHE_TTL_HOURS = 24; // 缓存24小时
private static final int START_HOUR = 9; // 开始检查时间 9:00
private static final int END_HOUR = 18; // 结束检查时间 18:00
@Autowired
private DeviceIntegrationService deviceIntegrationService;
@Autowired
private DeviceRepository deviceRepository;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private ObjectMapper objectMapper;
/**
* 定时任务:每5分钟执行一次
* cron表达式: 0 0/5 * * * * 表示每5分钟执行一次
*/
@Scheduled(cron = "0 0/5 * * * *")
public void checkDeviceVideoContinuity() {
// 检查是否在执行时间范围内(9:00-18:00)
int currentHour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
if (currentHour < START_HOUR || currentHour >= END_HOUR) {
log.debug("当前时间 {}:00 不在检查时间范围内({}:00-{}:00),跳过检查",
currentHour, START_HOUR, END_HOUR);
return;
}
log.info("开始执行设备视频连续性检查定时任务");
long startTime = System.currentTimeMillis();
try {
// 获取所有激活的设备(分页获取,每次100个)
int pageSize = 100;
int currentPage = 1;
int totalChecked = 0;
int successCount = 0;
int failureCount = 0;
while (true) {
PageResponse<DeviceV2DTO> pageResponse = deviceIntegrationService.listDevices(
currentPage, pageSize, null, null, null, 1, null
);
if (pageResponse == null || pageResponse.getList() == null
|| pageResponse.getList().isEmpty()) {
break;
}
// 检查每个设备的视频连续性
for (DeviceV2DTO device : pageResponse.getList()) {
try {
boolean checked = checkSingleDevice(device);
totalChecked++;
if (checked) {
successCount++;
} else {
failureCount++;
}
} catch (Exception e) {
log.error("检查设备 {} 视频连续性失败: {}", device.getId(), e.getMessage(), e);
failureCount++;
totalChecked++;
}
}
// 检查是否还有更多页
int totalPages = (int) Math.ceil((double) pageResponse.getTotal() / pageSize);
if (currentPage >= totalPages) {
break;
}
currentPage++;
}
long endTime = System.currentTimeMillis();
log.info("设备视频连续性检查任务完成: 总计检查 {} 个设备, 成功 {}, 失败 {}, 耗时 {}ms",
totalChecked, successCount, failureCount, (endTime - startTime));
} catch (Exception e) {
log.error("执行设备视频连续性检查定时任务失败", e);
}
}
/**
* 检查单个设备的视频连续性
*
* @param device 设备信息
* @return true表示检查成功并缓存,false表示跳过检查
*/
private boolean checkSingleDevice(DeviceV2DTO device) {
try {
// 获取设备配置
DeviceConfigEntity config = deviceRepository.getDeviceConfig(device.getId());
if (config == null) {
log.debug("设备 {} 没有配置信息,跳过检查", device.getId());
return false;
}
// 获取设备的存储操作器
IDeviceStorageOperator operator = DeviceFactory.getDeviceStorageOperator(device, config);
if (operator == null) {
log.debug("设备 {} 没有配置存储操作器,跳过检查", device.getId());
return false;
}
// 计算检查时间范围: 当前时间向前12分钟到向前2分钟
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, -2);
Date endDate = calendar.getTime();
calendar.add(Calendar.MINUTE, -10); // 再向前10分钟,总共12分钟
Date startDate = calendar.getTime();
// 执行连续性检查(允许2秒间隙)
VideoContinuityResult result = operator.checkVideoContinuity(startDate, endDate, 2000L);
// 创建缓存对象
DeviceVideoContinuityCache cache = DeviceVideoContinuityCache.fromResult(
device.getId(), result, startDate, endDate
);
// 存储到Redis
String redisKey = REDIS_KEY_PREFIX + device.getId();
String cacheJson = objectMapper.writeValueAsString(cache);
redisTemplate.opsForValue().set(redisKey, cacheJson, CACHE_TTL_HOURS, TimeUnit.HOURS);
log.info("设备 {} 视频连续性检查完成: support={}, continuous={}, videos={}, gaps={}, duration={}ms",
device.getId(), result.isSupport(), result.isContinuous(),
result.getTotalVideos(), result.getGapCount(), result.getTotalDurationMs());
return true;
} catch (Exception e) {
log.error("检查设备 {} 视频连续性失败", device.getId(), e);
throw new RuntimeException("检查设备视频连续性失败", e);
}
}
/**
* 手动触发检查(用于测试)
*
* @param deviceId 设备ID
* @return 检查结果
*/
public DeviceVideoContinuityCache manualCheck(Long deviceId) {
log.info("手动触发设备 {} 的视频连续性检查", deviceId);
try {
// 获取设备信息
DeviceV2DTO device = deviceIntegrationService.getDevice(deviceId);
if (device == null) {
throw new RuntimeException("设备不存在: " + deviceId);
}
// 检查设备
checkSingleDevice(device);
// 从Redis获取结果
String redisKey = REDIS_KEY_PREFIX + deviceId;
String cacheJson = redisTemplate.opsForValue().get(redisKey);
if (cacheJson == null) {
throw new RuntimeException("检查完成但未找到缓存结果");
}
return objectMapper.readValue(cacheJson, DeviceVideoContinuityCache.class);
} catch (Exception e) {
log.error("手动检查设备 {} 视频连续性失败", deviceId, e);
throw new RuntimeException("手动检查失败: " + e.getMessage(), e);
}
}
}