Преглед изворни кода

feat: 设备健康探测定时任务,主动查询故障设备状态并自动恢复

- 新增 DeviceHealthProbeService,通过 RRPC query_state 主动探测设备
- 新增 DeviceHealthProbeJob 定时任务,每3分钟扫描故障设备并探测
- 新增 POST /washDevice/remote/probeAndRecover 手动探测恢复端点
- 探测成功自动恢复设备状态、监控日志、故障记录及发送恢复通知

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline пре 1 дан
родитељ
комит
e5743cb967

+ 24 - 1
car-wash-admin/src/main/java/com/kym/admin/controller/WashDeviceRemoteController.java

@@ -7,6 +7,7 @@ import com.kym.entity.WashDevice;
 import com.kym.entity.awoara.OrderInfo;
 import com.kym.entity.awoara.response.HardwareInfo;
 import com.kym.entity.awoara.response.State;
+import com.kym.service.DeviceHealthProbeService;
 import com.kym.service.WashDeviceService;
 import com.kym.service.awoara.AwoaraService;
 import lombok.extern.slf4j.Slf4j;
@@ -26,10 +27,13 @@ public class WashDeviceRemoteController {
 
     private final WashDeviceService washDeviceService;
     private final AwoaraService awoaraService;
+    private final DeviceHealthProbeService deviceHealthProbeService;
 
-    public WashDeviceRemoteController(WashDeviceService washDeviceService, AwoaraService awoaraService) {
+    public WashDeviceRemoteController(WashDeviceService washDeviceService, AwoaraService awoaraService,
+                                       DeviceHealthProbeService deviceHealthProbeService) {
         this.washDeviceService = washDeviceService;
         this.awoaraService = awoaraService;
+        this.deviceHealthProbeService = deviceHealthProbeService;
     }
 
     /**
@@ -168,6 +172,25 @@ public class WashDeviceRemoteController {
         return R.success();
     }
 
+    /**
+     * 探测设备连通性,在线则自动恢复设备状态和故障记录
+     */
+    @SaCheckPermission("washDevice.modify")
+    @PostMapping("/probeAndRecover")
+    R<?> probeAndRecover(@RequestBody Map<String, String> params) {
+        String productKey = params.get("productKey");
+        String deviceName = params.get("deviceName");
+        var device = washDeviceService.lambdaQuery()
+                .eq(WashDevice::getProductKey, productKey)
+                .eq(WashDevice::getDeviceName, deviceName)
+                .one();
+        if (device == null) {
+            return R.failed(400, "设备不存在");
+        }
+        boolean recovered = deviceHealthProbeService.probeAndRecover(device);
+        return recovered ? R.success("设备在线,已触发恢复") : R.failed(400, "设备离线,未能恢复");
+    }
+
     /**
      * 强制关闭订单
      */

+ 61 - 0
car-wash-admin/src/main/java/com/kym/admin/jobs/DeviceHealthProbeJob.java

@@ -0,0 +1,61 @@
+package com.kym.admin.jobs;
+
+import com.kym.entity.WashDevice;
+import com.kym.service.DeviceHealthProbeService;
+import com.kym.service.WashDeviceService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 设备健康探测定时任务
+ * 主动查询故障状态设备的连通性,探测到设备在线则触发自动恢复
+ *
+ * @author skyline
+ */
+@Slf4j
+@Component
+public class DeviceHealthProbeJob {
+
+    private final WashDeviceService washDeviceService;
+    private final DeviceHealthProbeService deviceHealthProbeService;
+
+    public DeviceHealthProbeJob(WashDeviceService washDeviceService,
+                                DeviceHealthProbeService deviceHealthProbeService) {
+        this.washDeviceService = washDeviceService;
+        this.deviceHealthProbeService = deviceHealthProbeService;
+    }
+
+    /**
+     * 每3分钟扫描所有故障状态设备,主动探测连通性
+     */
+    @Scheduled(cron = "0 */3 * * * ?")
+    public void probeFaultDevices() {
+        log.debug("设备健康探测-开始");
+        List<WashDevice> faultDevices = washDeviceService.lambdaQuery()
+                .eq(WashDevice::getState, WashDevice.STATE_故障)
+                .list();
+
+        if (faultDevices.isEmpty()) {
+            log.debug("设备健康探测-无故障设备");
+            return;
+        }
+
+        log.info("设备健康探测-发现 {} 个故障设备,开始探测", faultDevices.size());
+        int recovered = 0;
+
+        for (WashDevice device : faultDevices) {
+            try {
+                if (deviceHealthProbeService.probeAndRecover(device)) {
+                    recovered++;
+                }
+            } catch (Exception e) {
+                log.error("设备健康探测-设备 {} 探测异常", device.getDeviceName(), e);
+            }
+        }
+
+        log.info("设备健康探测-结束,共探测 {} 个故障设备,恢复 {} 个", faultDevices.size(), recovered);
+    }
+}

+ 19 - 0
car-wash-service/src/main/java/com/kym/service/DeviceHealthProbeService.java

@@ -0,0 +1,19 @@
+package com.kym.service;
+
+import com.kym.entity.WashDevice;
+
+/**
+ * 设备健康探测服务——主动查询设备状态并自动恢复
+ *
+ * @author skyline
+ */
+public interface DeviceHealthProbeService {
+
+    /**
+     * 主动探测设备状态,如果设备在线且正常则触发恢复
+     *
+     * @param device 设备实体
+     * @return true=设备在线已恢复, false=设备仍然离线
+     */
+    boolean probeAndRecover(WashDevice device);
+}

+ 86 - 0
car-wash-service/src/main/java/com/kym/service/impl/DeviceHealthProbeServiceImpl.java

@@ -0,0 +1,86 @@
+package com.kym.service.impl;
+
+import com.kym.entity.MonitorLog;
+import com.kym.entity.WashDevice;
+import com.kym.entity.common.RedisKeys;
+import com.kym.service.*;
+import com.kym.service.awoara.AwoaraService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+/**
+ * 设备健康探测服务实现
+ *
+ * @author skyline
+ */
+@Slf4j
+@Service
+public class DeviceHealthProbeServiceImpl implements DeviceHealthProbeService {
+
+    private final WashDeviceService washDeviceService;
+    private final MonitorLogService monitorLogService;
+    private final AwoaraService awoaraService;
+    private final StringRedisTemplate stringRedisTemplate;
+    private final FaultNotificationService faultNotificationService;
+
+    public DeviceHealthProbeServiceImpl(WashDeviceService washDeviceService,
+                                        MonitorLogService monitorLogService,
+                                        AwoaraService awoaraService,
+                                        StringRedisTemplate stringRedisTemplate,
+                                        FaultNotificationService faultNotificationService) {
+        this.washDeviceService = washDeviceService;
+        this.monitorLogService = monitorLogService;
+        this.awoaraService = awoaraService;
+        this.stringRedisTemplate = stringRedisTemplate;
+        this.faultNotificationService = faultNotificationService;
+    }
+
+    @Override
+    public boolean probeAndRecover(WashDevice device) {
+        try {
+            awoaraService.queryState(device.getProductKey(), device.getDeviceName());
+            log.info("设备探测成功,触发恢复: deviceId={}, deviceName={}", device.getId(), device.getDeviceName());
+            recoverDevice(device);
+            return true;
+        } catch (Exception e) {
+            log.debug("设备探测失败(仍离线): deviceId={}, deviceName={}, error={}",
+                    device.getId(), device.getDeviceName(), e.getMessage());
+            return false;
+        }
+    }
+
+    private void recoverDevice(WashDevice device) {
+        var deviceId = device.getId().toString();
+        var deviceName = device.getDeviceName();
+
+        // 更新设备状态为空闲
+        washDeviceService.lambdaUpdate()
+                .set(WashDevice::getState, WashDevice.STATE_空闲)
+                .eq(WashDevice::getId, device.getId())
+                .update();
+
+        // 清除离线标记
+        stringRedisTemplate.delete(RedisKeys.OFFLINE + deviceId);
+
+        // 恢复监控日志
+        var unrecoveredLog = monitorLogService.lambdaQuery()
+                .eq(MonitorLog::getSn, deviceName)
+                .eq(MonitorLog::getIsRecover, MonitorLog.IS_RECOVER_未恢复)
+                .orderByDesc(MonitorLog::getOfflineTime)
+                .last("limit 1")
+                .one();
+        if (unrecoveredLog != null) {
+            monitorLogService.lambdaUpdate()
+                    .set(MonitorLog::getIsRecover, MonitorLog.IS_RECOVER_已恢复)
+                    .set(MonitorLog::getRecoverTime, LocalDateTime.now())
+                    .eq(MonitorLog::getId, unrecoveredLog.getId())
+                    .update();
+        }
+
+        // 恢复故障记录并发送通知
+        faultNotificationService.handleOnlineRecovery(device.getStationId(), deviceName);
+    }
+}