|
@@ -0,0 +1,195 @@
|
|
|
+package vip.xiaonuo.coldchain.core.alarm.offline;
|
|
|
+
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.jetbrains.annotations.NotNull;
|
|
|
+import org.springframework.context.ApplicationEventPublisher;
|
|
|
+import org.springframework.scheduling.annotation.Async;
|
|
|
+import org.springframework.scheduling.annotation.Scheduled;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import vip.xiaonuo.coldchain.core.alarm.bean.SensorAlarm;
|
|
|
+import vip.xiaonuo.coldchain.core.alarm.bean.SensorAlarmUser;
|
|
|
+import vip.xiaonuo.coldchain.core.alarm.config.ColdChainAlarmMessageProperties;
|
|
|
+import vip.xiaonuo.coldchain.core.config.JfcloudColdChainConstants;
|
|
|
+import vip.xiaonuo.coldchain.core.event.SensorAlarmEvent;
|
|
|
+import vip.xiaonuo.coldchain.core.renke.config.JfcloudColdChainServerProperties;
|
|
|
+import vip.xiaonuo.coldchain.modular.monitortargetregion.entity.MonitorTargetRegion;
|
|
|
+import vip.xiaonuo.coldchain.modular.monitortargetregion.service.MonitorTargetRegionService;
|
|
|
+import vip.xiaonuo.common.cache.CommonCacheOperator;
|
|
|
+
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * @author jackzhou
|
|
|
+ * @version 1.0
|
|
|
+ * @project jfcloud-coldchain
|
|
|
+ * @description
|
|
|
+ * @date 2025/1/5 10:50:04
|
|
|
+ */
|
|
|
+
|
|
|
+@RequiredArgsConstructor
|
|
|
+@Service
|
|
|
+@Slf4j
|
|
|
+public class DeviceOfflineDetectionService {
|
|
|
+ // 时间格式化器,用于格式化时间为指定格式
|
|
|
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
+ private static final String CACHE_KEY_PREFIX = "Cache:DeviceOffline:";
|
|
|
+ private final CommonCacheOperator commonCacheOperator;
|
|
|
+ private final ApplicationEventPublisher applicationEventPublisher;
|
|
|
+ private final MonitorTargetRegionService monitorTargetRegionService;
|
|
|
+ private final ColdChainAlarmMessageProperties alarmMessageProperties;
|
|
|
+ private final JfcloudColdChainServerProperties jfcloudColdChainServerProperties;
|
|
|
+ private static final String KEY_SPILT = "-";
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 异步上报设备数据
|
|
|
+ *
|
|
|
+ * @param deviceCode 设备标识符
|
|
|
+ */
|
|
|
+ @Async("coldChainAsyncTask")
|
|
|
+ public void reportDeviceTime(String deviceCode, Integer route) {
|
|
|
+ // 获取当前时间并格式化为字符串
|
|
|
+ String currentTime = LocalDateTime.now().format(DATE_TIME_FORMATTER);
|
|
|
+ // 将设备的上报时间存储到 Redis 中,并设置过期时间(离线阈值时间+1分钟)
|
|
|
+ cacheDeviceTime(deviceCode, route, currentTime);
|
|
|
+ log.info("设备 {} 异步上报数据,时间:{}\n", getKey(deviceCode, route), currentTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 定期检测设备状态是否离线
|
|
|
+ * 每 5 分钟运行一次
|
|
|
+ */
|
|
|
+ @Scheduled(fixedRate = JfcloudColdChainConstants.OFFLINE_THRESHOLD_SECONDS) // 每 5 分钟(以毫秒为单位)
|
|
|
+ public void checkDeviceStatus() {
|
|
|
+ log.info("开始检测离线设备状态...");
|
|
|
+ // 获取 Redis 中所有设备的键
|
|
|
+ Set<String> deviceKeys = getAllDeviceCodes();
|
|
|
+ if (deviceKeys == null || deviceKeys.isEmpty()) {
|
|
|
+ log.info("没有需要检测的设备。");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 遍历设备键,逐一检测状态
|
|
|
+ for (String key : deviceKeys) {
|
|
|
+ String[] split = key.split(KEY_SPILT);
|
|
|
+ if (split.length == 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String deviceCode = split[0];
|
|
|
+ Integer route = null;
|
|
|
+ if (split.length == 2) {
|
|
|
+ route = Integer.valueOf(split[1]);
|
|
|
+ }
|
|
|
+ // 从 Redis 中获取设备的最后上报时间
|
|
|
+ String lastReportTimeStr = getLastDeviceTime(getKey(deviceCode,route));
|
|
|
+ if (lastReportTimeStr != null) {
|
|
|
+ // 将上报时间字符串解析为 LocalDateTime 对象
|
|
|
+ LocalDateTime lastReportTime = LocalDateTime.parse(lastReportTimeStr, DATE_TIME_FORMATTER);
|
|
|
+ // 获取当前时间
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
+ final long OFFLINE_THRESHOLD_MINUTES = jfcloudColdChainServerProperties.getDeviceOfflineInterval() / 60 / 1000;
|
|
|
+ // 如果最后上报时间超过离线阈值,判断设备为离线
|
|
|
+ if (lastReportTime.plusMinutes(OFFLINE_THRESHOLD_MINUTES).isBefore(now)) {
|
|
|
+ log.error("设备{}-{} 已离线,最后上报时间:{}。\n", deviceCode, route, lastReportTimeStr);
|
|
|
+ publishAlarm(deviceCode, route, lastReportTimeStr);
|
|
|
+ // TODO 标识设备离线
|
|
|
+ } else {
|
|
|
+ // TODO 标识设备在线
|
|
|
+ log.info("设备 {}-{} 在线,最后上报时间:{}。\n", deviceCode, route, lastReportTimeStr);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果设备上报时间为空,认为设备可能从未上报过数据
|
|
|
+ log.info("设备 {}-{} 缺少数据,可能从未上报过。\n", deviceCode, route);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private void publishAlarm(String deviceCode, Integer route, String time) {
|
|
|
+ MonitorTargetRegion monitorTargetRegion = monitorTargetRegionService.findOneByDeviceCodeAndSensorNo(deviceCode, route);
|
|
|
+ if (Objects.isNull(monitorTargetRegion)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String monitorTargetId = monitorTargetRegion.getMonitorTargetId();
|
|
|
+ Integer sensorRoute = monitorTargetRegion.getSensorRoute();
|
|
|
+ String sensorCode = monitorTargetRegion.getSensorCode();
|
|
|
+ String deviceName = monitorTargetRegion.getName();
|
|
|
+ String deviceId = monitorTargetRegion.getId();
|
|
|
+ final String alarmType = "设备离线";
|
|
|
+ String alarmMessage = getAlarmMessage(deviceName, time);
|
|
|
+ SensorAlarm sensorAlarm = new SensorAlarm();
|
|
|
+ sensorAlarm.setAlarmType(alarmType);
|
|
|
+ sensorAlarm.setValue(0f);
|
|
|
+ sensorAlarm.setAlarmTime(time);
|
|
|
+ sensorAlarm.setSource(deviceName); // 设置设备名称
|
|
|
+ sensorAlarm.setDeviceName(deviceName); // 设置设备名称
|
|
|
+ sensorAlarm.setDeviceId(deviceId); // 设置设备名称
|
|
|
+ sensorAlarm.setPriority("高"); // 设置设备名称
|
|
|
+ sensorAlarm.setStatus("未处理"); // 设置设备名称
|
|
|
+ sensorAlarm.setAlarmTime(time); // 设置报警时间
|
|
|
+ sensorAlarm.setMessage(alarmMessage); // 设置报警消息
|
|
|
+ sensorAlarm.setThreshold(null); // 设置预警值
|
|
|
+ sensorAlarm.setMonitorTargetId(monitorTargetId);
|
|
|
+ sensorAlarm.setSensorRoute(sensorRoute);
|
|
|
+ sensorAlarm.setSensorCode(sensorCode);
|
|
|
+ sensorAlarm.setCreateUser(monitorTargetRegion.getCreateUser());
|
|
|
+ List<SensorAlarmUser> alarmUsers = monitorTargetRegion.getAlarmUsers();
|
|
|
+ sensorAlarm.setAlarmUsers(alarmUsers);
|
|
|
+ sensorAlarm.setThreshold(0f);
|
|
|
+ // 设置报警人机构 所属用户和机构
|
|
|
+ sensorAlarm.setCreateUser(monitorTargetRegion.getCreateUser());
|
|
|
+ sensorAlarm.setType("1");
|
|
|
+ log.warn("设备断电报警: 类型: {},详细报警内容 : {}", alarmType, alarmMessage);
|
|
|
+ // 发布报警事件
|
|
|
+ applicationEventPublisher.publishEvent(new SensorAlarmEvent(this, sensorAlarm));
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getAlarmMessage(String deviceName, String time) {
|
|
|
+ String messageTemplate = alarmMessageProperties.getDeviceOfflineLimit();
|
|
|
+ return messageTemplate.replace("{deviceName}", deviceName).replace("{time}", time);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 缓存设备时间
|
|
|
+ *
|
|
|
+ * @param deviceCode
|
|
|
+ * @param route
|
|
|
+ * @param time
|
|
|
+ */
|
|
|
+ private void cacheDeviceTime(String deviceCode, Integer route, String time) {
|
|
|
+ final long OFFLINE_THRESHOLD_MINUTES = jfcloudColdChainServerProperties.getDeviceOfflineInterval() / 60 / 1000;
|
|
|
+ commonCacheOperator.put(getKey(deviceCode, route), time/*, OFFLINE_THRESHOLD_MINUTES + 1, TimeUnit.MINUTES*/);
|
|
|
+ }
|
|
|
+
|
|
|
+ @NotNull
|
|
|
+ private static String getKey(String deviceCode, Integer route) {
|
|
|
+ if (Objects.nonNull(route)) {
|
|
|
+ return CACHE_KEY_PREFIX + deviceCode + KEY_SPILT + route;
|
|
|
+ }
|
|
|
+ return CACHE_KEY_PREFIX + deviceCode;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getLastDeviceTime(String key) {
|
|
|
+ if (exists(key)) {
|
|
|
+ return (String) commonCacheOperator.get(key);
|
|
|
+ } else {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean exists(String key) {
|
|
|
+ return commonCacheOperator.get(key) != null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Set<String> getAllDeviceCodes() {
|
|
|
+ return commonCacheOperator.getAllKeys().stream()
|
|
|
+ .filter(key -> key.startsWith(CACHE_KEY_PREFIX)) // 过滤指定前缀的键
|
|
|
+ .map(key -> key.replace(CACHE_KEY_PREFIX, "")) // 去掉前缀
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ }
|
|
|
+}
|