You've already forked FrameTour-BE
- 在 PhotoProcessContext 中新增 imageRotation 字段用于存储旋转角度 - 修改 ConditionalRotateStage 支持 90、180、270 度旋转 - 优化 ImageOrientationStage 综合判断图片方向逻辑 - 新增 NoOpStage 作为空操作阶段占位符 - 解除 DeviceVideoContinuityCheckTask 的生产环境限制 - 添加完整的单元测试覆盖各种旋转场景和边界情况
217 lines
8.2 KiB
Java
217 lines
8.2 KiB
Java
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);
|
|
}
|
|
}
|
|
}
|