diff --git a/readme.md b/readme.md index 3c9d0db..d2d1630 100644 --- a/readme.md +++ b/readme.md @@ -1 +1 @@ -ddddddddddddddddddddddddddddddddddddddddddddddD堆堆ddddddddddddddddddddeeedddddddd +ddddddddddddddddddddddddddddddddddddddddddddddddD堆堆ddddddddddddddddddddeeedddddddd diff --git a/src/main/java/com/ruoyi/device/domain/impl/machine/mqtt/MqttCallbackRegistry.java b/src/main/java/com/ruoyi/device/domain/impl/machine/mqtt/MqttCallbackRegistry.java index c76f5d9..b68bb10 100644 --- a/src/main/java/com/ruoyi/device/domain/impl/machine/mqtt/MqttCallbackRegistry.java +++ b/src/main/java/com/ruoyi/device/domain/impl/machine/mqtt/MqttCallbackRegistry.java @@ -158,13 +158,13 @@ public class MqttCallbackRegistry { // 如果是 confirm 或 realTime/data 消息,打印详细日志 if (topic.contains("/control/confirm") || topic.contains("/realTime/data")) { - log.info("【MqttCallbackRegistry】处理消息: topic={}, callbackCount={}, messageBody={}", + log.info("【Machine MqttCallbackRegistry】处理消息: topic={}, callbackCount={}, messageBody={}", topic, callbacks.size(), messageBody); } if (callbacks.isEmpty()) { if (topic.contains("/control/confirm") || topic.contains("/realTime/data")) { - log.warn("【MqttCallbackRegistry】没有找到匹配的回调: topic={}", topic); + log.debug("【Machine MqttCallbackRegistry】没有找到匹配的回调: topic={}", topic); } return; } diff --git a/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/callback/IAirportFlyControlDataCallback.java b/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/callback/IAirportFlyControlDataCallback.java new file mode 100644 index 0000000..e100c91 --- /dev/null +++ b/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/callback/IAirportFlyControlDataCallback.java @@ -0,0 +1,5 @@ +package com.ruoyi.device.domain.impl.tuohengmqtt.callback; + +public interface IAirportFlyControlDataCallback { + void onAirportFlyControlData(String deviceSn, String payload, String topic); +} diff --git a/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/handler/TuohengMqttMessageHandler.java b/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/handler/TuohengMqttMessageHandler.java index a43c12d..ef9192f 100644 --- a/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/handler/TuohengMqttMessageHandler.java +++ b/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/handler/TuohengMqttMessageHandler.java @@ -5,6 +5,7 @@ import com.ruoyi.device.domain.impl.machine.mqtt.MqttCallbackRegistry; import com.ruoyi.device.domain.impl.tuohengmqtt.callback.IDroneRealTimeCallback; import com.ruoyi.device.domain.impl.tuohengmqtt.callback.IHeartbeatMessageCallback; import com.ruoyi.device.domain.impl.tuohengmqtt.callback.IRealTimeBasicCallback; +import com.ruoyi.device.domain.impl.tuohengmqtt.callback.IAirportFlyControlDataCallback; import com.ruoyi.device.domain.impl.tuohengmqtt.callback.ITuohengEventsCallback; import com.ruoyi.device.domain.impl.tuohengmqtt.callback.ITuohengOsdCallback; import com.ruoyi.device.domain.impl.tuohengmqtt.callback.ITuohengRealTimeDataCallback; @@ -21,12 +22,14 @@ import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; +import static com.ruoyi.device.domain.impl.tuohengmqtt.service.TuohengMqttClientService.*; + @Slf4j @Component public class TuohengMqttMessageHandler { private final ObjectMapper objectMapper = new ObjectMapper(); - private MqttCallbackRegistry mqttCallbackRegistry; + private MqttCallbackRegistry machineCallBackRegistry; private final List realTimeDataCallbacks = new ArrayList<>(); private final List osdCallbacks = new ArrayList<>(); @@ -34,15 +37,17 @@ public class TuohengMqttMessageHandler { private final List realTimeBasicCallbacks = new ArrayList<>(); private final List droneRealTimeCallbacks = new ArrayList<>(); private final List heartbeatMessageCallbacks = new ArrayList<>(); + private final List airportFlyControlDataCallbacks = new ArrayList<>(); private static final Pattern TUOHENG_SN_PATTERN = Pattern.compile("^TH[0-9A-Z]+"); + /** * 设置 MQTT 回调注册中心 */ - public void setMqttCallbackRegistry(MqttCallbackRegistry mqttCallbackRegistry) { - this.mqttCallbackRegistry = mqttCallbackRegistry; - log.info("设置 MqttCallbackRegistry 成功"); + public void setMachineCallBackRegistry(MqttCallbackRegistry machineCallBackRegistry) { + this.machineCallBackRegistry = machineCallBackRegistry; + log.info("设置 machineCallBackRegistry 成功"); } public void registerRealTimeDataCallback(ITuohengRealTimeDataCallback callback) { @@ -87,38 +92,34 @@ public class TuohengMqttMessageHandler { } } + public void registerAirportFlyControlDataCallback(IAirportFlyControlDataCallback callback) { + if (callback != null && !airportFlyControlDataCallbacks.contains(callback)) { + airportFlyControlDataCallbacks.add(callback); + log.info("注册机场飞行控制数据回调: {}", callback.getClass().getSimpleName()); + } + } + public void handleMessage(String topic, String payload) { try { log.debug("收到MQTT消息 - Topic: {}", topic); - // 如果是 confirm 消息,打印详细日志 - if (topic.contains("/control/confirm")) { - log.info("【收到confirm消息】Topic: {}, Payload: {}", topic, payload); - } - // 通知 MqttCallbackRegistry 处理回调(用于指令回调) - if (mqttCallbackRegistry != null) { + if (machineCallBackRegistry != null) { try { // 将 payload 解析为 JSON 对象传递给回调注册中心 Object messageBody = objectMapper.readValue(payload, Object.class); - mqttCallbackRegistry.handleMessage(topic, messageBody); - - // 如果是 confirm 消息,打印回调处理结果 - if (topic.contains("/control/confirm")) { - log.info("【confirm消息已传递给回调注册中心】Topic: {}", topic); - } + machineCallBackRegistry.handleMessage(topic, messageBody); } catch (Exception e) { log.debug("通知回调注册中心失败: {}", e.getMessage()); } } String deviceSn = extractDeviceSnFromTopic(topic); - if (deviceSn == null) { log.warn("无法从Topic解析设备SN: {}", topic); return; } - + if (isProductTopic(topic)) { if (!isTuohengSn(deviceSn)) { log.debug("跳过大疆设备 - SN: {}", deviceSn); @@ -126,31 +127,29 @@ public class TuohengMqttMessageHandler { } } - String messageType = extractMessageTypeFromTopic(topic); - - if (messageType == null) { - log.warn("无法从Topic解析消息类型: {}", topic); - return; - } - - switch (messageType) { - case "realTime/data": - handleRealTimeData(deviceSn, payload, topic); - break; - case "realTime/basic": - handleRealTimeBasicData(deviceSn, payload); - break; - case "heartbeat/message": - handleHeartbeatMessage(deviceSn, payload); - break; - case "osd": - handleOsdData(deviceSn, payload); - break; - case "events": - handleEventsData(deviceSn, payload); - break; - default: - log.debug("未知消息类型: {}", messageType); + // 根据Topic模式匹配分发到不同的处理方法 + if (matchTopic(topic, AIRPORT_NEST_REALTIME_TOPIC)) { + handleAirportNestRealTimeData(deviceSn, payload); + } else if (matchTopic(topic, AIRPORT_NEST_BASIC_TOPIC)) { + handleRealTimeBasicData(deviceSn, payload); + } else if (matchTopic(topic, AIRPORT_NEST_CONFIRM_TOPIC)) { + // 机场控制确认消息(已由 MqttCallbackRegistry 处理) + log.debug("机场控制确认消息 - SN: {}", deviceSn); + } else if (matchTopic(topic, AIRPORT_DRONE_REALTIME_TOPIC)) { + handleAirportDroneRealTimeData(deviceSn, payload); + } else if (matchTopic(topic, AIRPORT_FLY_DATA_TOPIC)) { + handleAirportFlyControlData(deviceSn, payload, topic); + } else if (matchTopic(topic, AIRPORT_FLY_CONFIRM_TOPIC)) { + // 飞行控制确认消息(已由 MqttCallbackRegistry 处理) + log.debug("飞行控制确认消息 - SN: {}", deviceSn); + } else if (matchTopic(topic, HEARTBEAT_MESSAGE_TOPIC)) { + handleHeartbeatMessage(deviceSn, payload); + } else if (matchTopic(topic, PRODUCT_OSD_TOPIC)) { + handleOsdData(deviceSn, payload); + } else if (matchTopic(topic, PRODUCT_EVENTS_TOPIC)) { + handleEventsData(deviceSn, payload); + } else { + log.debug("未知的消息类型 - Topic: {}", topic); } } catch (Exception e) { @@ -158,31 +157,35 @@ public class TuohengMqttMessageHandler { } } - private void handleRealTimeData(String deviceSn, String payload, String topic) { + private void handleAirportNestRealTimeData(String deviceSn, String payload) { try { - if (topic.contains("airportNest")) { - TuohengRealTimeData data = objectMapper.readValue(payload, TuohengRealTimeData.class); - log.debug("处理机场实时数据 - 设备SN: {}", deviceSn); - for (ITuohengRealTimeDataCallback callback : realTimeDataCallbacks) { - try { - callback.onRealTimeData(deviceSn, data); - } catch (Exception e) { - log.error("实时数据回调执行失败: {}", e.getMessage(), e); - } - } - } else if (topic.contains("airportDrone")) { - DroneRealTimeData data = objectMapper.readValue(payload, DroneRealTimeData.class); - log.debug("处理无人机实时数据 - 设备SN: {}", deviceSn); - for (IDroneRealTimeCallback callback : droneRealTimeCallbacks) { - try { - callback.onDroneRealTimeData(deviceSn, data); - } catch (Exception e) { - log.error("无人机实时数据回调执行失败: {}", e.getMessage(), e); - } + TuohengRealTimeData data = objectMapper.readValue(payload, TuohengRealTimeData.class); + log.debug("处理机场实时数据 - 设备SN: {}", deviceSn); + for (ITuohengRealTimeDataCallback callback : realTimeDataCallbacks) { + try { + callback.onRealTimeData(deviceSn, data); + } catch (Exception e) { + log.error("实时数据回调执行失败: {}", e.getMessage(), e); } } } catch (Exception e) { - log.error("处理实时数据失败 - SN: {}, Error: {}", deviceSn, e.getMessage(), e); + log.error("处理机场实时数据失败 - SN: {}, Error: {}", deviceSn, e.getMessage(), e); + } + } + + private void handleAirportDroneRealTimeData(String deviceSn, String payload) { + try { + DroneRealTimeData data = objectMapper.readValue(payload, DroneRealTimeData.class); + log.debug("处理无人机实时数据 - 设备SN: {}", deviceSn); + for (IDroneRealTimeCallback callback : droneRealTimeCallbacks) { + try { + callback.onDroneRealTimeData(deviceSn, data); + } catch (Exception e) { + log.error("无人机实时数据回调执行失败: {}", e.getMessage(), e); + } + } + } catch (Exception e) { + log.error("处理无人机实时数据失败 - SN: {}, Error: {}", deviceSn, e.getMessage(), e); } } @@ -254,6 +257,22 @@ public class TuohengMqttMessageHandler { } } + private void handleAirportFlyControlData(String deviceSn, String payload, String topic) { + try { + log.info("处理机场飞行控制数据 - 设备SN: {}, Topic: {}", deviceSn, topic); + + for (IAirportFlyControlDataCallback callback : airportFlyControlDataCallbacks) { + try { + callback.onAirportFlyControlData(deviceSn, payload, topic); + } catch (Exception e) { + log.error("机场飞行控制数据回调执行失败: {}", e.getMessage(), e); + } + } + } catch (Exception e) { + log.error("处理机场飞行控制数据失败 - SN: {}, Error: {}", deviceSn, e.getMessage(), e); + } + } + private String extractDeviceSnFromTopic(String topic) { if (topic == null) { return null; @@ -277,38 +296,7 @@ public class TuohengMqttMessageHandler { return null; } - private String extractMessageTypeFromTopic(String topic) { - if (topic == null) { - return null; - } - String[] parts = topic.split("/"); - - // /topic/v1/heartbeat/{deviceSn}/message - // parts[0]="", parts[1]="topic", parts[2]="v1", parts[3]="heartbeat", - // parts[4]=deviceSn, parts[5]="message" - if (topic.startsWith("/topic/v1/heartbeat/")) { - if (parts.length >= 6) { - return "heartbeat/message"; - } - } - // /topic/v1/airportNest/{deviceSn}/realTime/data - // parts[0]="", parts[1]="topic", parts[2]="v1", parts[3]="airportNest", - // parts[4]=deviceSn, parts[5]="realTime", parts[6]="data" - else if (topic.startsWith("/topic/v1/")) { - if (parts.length >= 7) { - return parts[5] + "/" + parts[6]; // "realTime/data" or "realTime/basic" - } - } - // thing/product/{deviceSn}/osd - // parts[0]="thing", parts[1]="product", parts[2]=deviceSn, parts[3]="osd" - else if (topic.startsWith("thing/product/")) { - if (parts.length >= 4) { - return parts[3]; // "osd" or "events" - } - } - return null; - } - + private boolean isProductTopic(String topic) { return topic != null && topic.startsWith("thing/product/"); } @@ -319,4 +307,19 @@ public class TuohengMqttMessageHandler { } return TUOHENG_SN_PATTERN.matcher(sn).matches(); } + + /** + * 匹配Topic模式 + * @param topic 实际topic + * @param pattern 模式topic(支持+通配符) + * @return 是否匹配 + */ + private boolean matchTopic(String topic, String pattern) { + if (topic == null || pattern == null) { + return false; + } + // 将模式中的+替换为正则表达式(匹配除斜杠外的任意字符) + String regex = pattern.replace("+", "[^/]+"); + return topic.matches(regex); + } } diff --git a/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/model/DroneRealTimeData.java b/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/model/DroneRealTimeData.java index 8c4859a..d05e965 100644 --- a/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/model/DroneRealTimeData.java +++ b/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/model/DroneRealTimeData.java @@ -14,6 +14,9 @@ public class DroneRealTimeData { @JsonProperty("code") private Integer code; + @JsonProperty("messageID") + private String messageID; + @JsonProperty("data") private DroneInfo data; @@ -142,5 +145,8 @@ public class DroneRealTimeData { @JsonProperty("flowRate") private String flowRate; + + @JsonProperty("jiancha") + private String jiancha; } } diff --git a/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/service/TuohengMqttClientService.java b/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/service/TuohengMqttClientService.java index 33ba680..5d1010f 100644 --- a/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/service/TuohengMqttClientService.java +++ b/src/main/java/com/ruoyi/device/domain/impl/tuohengmqtt/service/TuohengMqttClientService.java @@ -20,13 +20,15 @@ public class TuohengMqttClientService { private final TuohengMqttMessageHandler messageHandler; private MqttClient mqttClient; - private static final String AIRPORT_NEST_REALTIME_TOPIC = "/topic/v1/airportNest/+/realTime/data"; - private static final String AIRPORT_NEST_BASIC_TOPIC = "/topic/v1/airportNest/+/realTime/basic"; - private static final String AIRPORT_NEST_CONFIRM_TOPIC = "/topic/v1/airportNest/+/control/confirm"; - private static final String AIRPORT_DRONE_REALTIME_TOPIC = "/topic/v1/airportDrone/+/realTime/data"; - private static final String HEARTBEAT_MESSAGE_TOPIC = "/topic/v1/heartbeat/+/message"; - private static final String PRODUCT_OSD_TOPIC = "thing/product/+/osd"; - private static final String PRODUCT_EVENTS_TOPIC = "thing/product/+/events"; + public static final String AIRPORT_NEST_REALTIME_TOPIC = "/topic/v1/airportNest/+/realTime/data"; + public static final String AIRPORT_NEST_BASIC_TOPIC = "/topic/v1/airportNest/+/realTime/basic"; + public static final String AIRPORT_NEST_CONFIRM_TOPIC = "/topic/v1/airportNest/+/control/confirm"; + public static final String AIRPORT_DRONE_REALTIME_TOPIC = "/topic/v1/airportDrone/+/realTime/data"; + public static final String AIRPORT_FLY_DATA_TOPIC = "/topic/v1/airportFly/+/control/data"; + public static final String AIRPORT_FLY_CONFIRM_TOPIC = "/topic/v1/airportFly/+/control/confirm"; + public static final String HEARTBEAT_MESSAGE_TOPIC = "/topic/v1/heartbeat/+/message"; + public static final String PRODUCT_OSD_TOPIC = "thing/product/+/osd"; + public static final String PRODUCT_EVENTS_TOPIC = "thing/product/+/events"; public TuohengMqttClientService(TuohengMqttClientConfig config, TuohengMqttMessageHandler messageHandler) { this.config = config; @@ -124,6 +126,8 @@ public class TuohengMqttClientService { AIRPORT_NEST_BASIC_TOPIC, AIRPORT_NEST_CONFIRM_TOPIC, AIRPORT_DRONE_REALTIME_TOPIC, + AIRPORT_FLY_DATA_TOPIC, + AIRPORT_FLY_CONFIRM_TOPIC, HEARTBEAT_MESSAGE_TOPIC, PRODUCT_OSD_TOPIC, PRODUCT_EVENTS_TOPIC diff --git a/src/main/java/com/ruoyi/device/mapper/FlightLogMapper.java b/src/main/java/com/ruoyi/device/mapper/FlightLogMapper.java new file mode 100644 index 0000000..f5c7819 --- /dev/null +++ b/src/main/java/com/ruoyi/device/mapper/FlightLogMapper.java @@ -0,0 +1,40 @@ +package com.ruoyi.device.mapper; + +import com.ruoyi.device.mapper.entity.FlightLogEntity; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 飞行日志表Mapper接口 + * + * @author ruoyi + * @date 2026-02-25 + */ +@Mapper +public interface FlightLogMapper +{ + /** + * 根据日志主键查询飞行日志 + * + * @param logId 日志主键 + * @return 飞行日志信息 + */ + FlightLogEntity selectFlightLogByLogId(Long logId); + + /** + * 根据飞行ID查询飞行日志列表 + * + * @param flightId 飞行ID + * @return 飞行日志集合 + */ + List selectFlightLogListByFlightId(Long flightId); + + /** + * 新增飞行日志 + * + * @param flightLog 飞行日志信息 + * @return 影响行数 + */ + int insertFlightLog(FlightLogEntity flightLog); +} diff --git a/src/main/java/com/ruoyi/device/mapper/FlightMapper.java b/src/main/java/com/ruoyi/device/mapper/FlightMapper.java new file mode 100644 index 0000000..31d6e42 --- /dev/null +++ b/src/main/java/com/ruoyi/device/mapper/FlightMapper.java @@ -0,0 +1,73 @@ +package com.ruoyi.device.mapper; + +import com.ruoyi.device.mapper.entity.FlightEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 飞行表Mapper接口 + * + * @author ruoyi + * @date 2026-02-25 + */ +@Mapper +public interface FlightMapper +{ + /** + * 根据飞行主键查询飞行记录 + * + * @param flightId 飞行主键 + * @return 飞行信息 + */ + FlightEntity selectFlightByFlightId(Long flightId); + + /** + * 根据设备SN查询最新的飞行记录 + * + * @param deviceSn 设备SN号 + * @return 飞行信息 + */ + FlightEntity selectLatestFlightByDeviceSn(String deviceSn); + + /** + * 根据设备SN和状态查询飞行记录 + * + * @param deviceSn 设备SN号 + * @param status 状态 + * @return 飞行信息 + */ + FlightEntity selectFlightByDeviceSnAndStatus(@Param("deviceSn") String deviceSn, @Param("status") String status); + + /** + * 新增飞行记录 + * + * @param flight 飞行信息 + * @return 影响行数 + */ + int insertFlight(FlightEntity flight); + + /** + * 修改飞行记录 + * + * @param flight 飞行信息 + * @return 影响行数 + */ + int updateFlight(FlightEntity flight); + + /** + * 更新飞行状态 + * + * @param flightId 飞行主键 + * @param status 状态 + * @return 影响行数 + */ + int updateFlightStatus(@Param("flightId") Long flightId, @Param("status") String status); + + /** + * 更新返航时间 + * + * @param flightId 飞行主键 + * @return 影响行数 + */ + int updateReturnTime(Long flightId); +} diff --git a/src/main/java/com/ruoyi/device/mapper/PreCheckLogMapper.java b/src/main/java/com/ruoyi/device/mapper/PreCheckLogMapper.java new file mode 100644 index 0000000..3957b97 --- /dev/null +++ b/src/main/java/com/ruoyi/device/mapper/PreCheckLogMapper.java @@ -0,0 +1,49 @@ +package com.ruoyi.device.mapper; + +import com.ruoyi.device.mapper.entity.PreCheckLogEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 自检日志表Mapper接口 + * + * @author ruoyi + * @date 2026-02-25 + */ +@Mapper +public interface PreCheckLogMapper +{ + /** + * 根据日志主键查询自检日志 + * + * @param logId 日志主键 + * @return 自检日志信息 + */ + PreCheckLogEntity selectPreCheckLogByLogId(Long logId); + + /** + * 根据飞行ID查询自检日志列表 + * + * @param flightId 飞行ID + * @return 自检日志集合 + */ + List selectPreCheckLogListByFlightId(Long flightId); + + /** + * 新增自检日志 + * + * @param preCheckLog 自检日志信息 + * @return 影响行数 + */ + int insertPreCheckLog(PreCheckLogEntity preCheckLog); + + /** + * 批量新增自检日志 + * + * @param preCheckLogList 自检日志集合 + * @return 影响行数 + */ + int insertPreCheckLogBatch(List preCheckLogList); +} diff --git a/src/main/java/com/ruoyi/device/mapper/entity/FlightEntity.java b/src/main/java/com/ruoyi/device/mapper/entity/FlightEntity.java new file mode 100644 index 0000000..249245c --- /dev/null +++ b/src/main/java/com/ruoyi/device/mapper/entity/FlightEntity.java @@ -0,0 +1,92 @@ +package com.ruoyi.device.mapper.entity; + +import com.ruoyi.common.core.web.domain.BaseEntity; + +/** + * 飞行表实体对象 device_flight + * Mapper 层实体,对应数据库表 + * + * @author ruoyi + * @date 2026-02-25 + */ +public class FlightEntity extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 飞行主键 */ + private Long flightId; + + /** 无人机SN */ + private String deviceSn; + + /** 外部飞行ID (来自MQTT的taskId) */ + private String flightIdExternal; + + /** 状态:自检中、飞行中、已返航 */ + private String status; + + /** 返航时间 */ + private java.util.Date returnTime; + + public Long getFlightId() + { + return flightId; + } + + public void setFlightId(Long flightId) + { + this.flightId = flightId; + } + + public String getDeviceSn() + { + return deviceSn; + } + + public void setDeviceSn(String deviceSn) + { + this.deviceSn = deviceSn; + } + + public String getFlightIdExternal() + { + return flightIdExternal; + } + + public void setFlightIdExternal(String flightIdExternal) + { + this.flightIdExternal = flightIdExternal; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public java.util.Date getReturnTime() + { + return returnTime; + } + + public void setReturnTime(java.util.Date returnTime) + { + this.returnTime = returnTime; + } + + @Override + public String toString() + { + return "FlightEntity{" + + "flightId=" + flightId + + ", deviceSn='" + deviceSn + '\'' + + ", flightIdExternal='" + flightIdExternal + '\'' + + ", status='" + status + '\'' + + ", returnTime=" + returnTime + + '}'; + } +} diff --git a/src/main/java/com/ruoyi/device/mapper/entity/FlightLogEntity.java b/src/main/java/com/ruoyi/device/mapper/entity/FlightLogEntity.java new file mode 100644 index 0000000..891094b --- /dev/null +++ b/src/main/java/com/ruoyi/device/mapper/entity/FlightLogEntity.java @@ -0,0 +1,64 @@ +package com.ruoyi.device.mapper.entity; + +import com.ruoyi.common.core.web.domain.BaseEntity; + +/** + * 飞行日志表实体对象 device_flight_log + * Mapper 层实体,对应数据库表 + * + * @author ruoyi + * @date 2026-02-25 + */ +public class FlightLogEntity extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 飞行日志主键 */ + private Long logId; + + /** 关联飞行表ID */ + private Long flightId; + + /** 日志内容 */ + private String logContent; + + public Long getLogId() + { + return logId; + } + + public void setLogId(Long logId) + { + this.logId = logId; + } + + public Long getFlightId() + { + return flightId; + } + + public void setFlightId(Long flightId) + { + this.flightId = flightId; + } + + public String getLogContent() + { + return logContent; + } + + public void setLogContent(String logContent) + { + this.logContent = logContent; + } + + @Override + public String toString() + { + return "FlightLogEntity{" + + "logId=" + logId + + ", flightId=" + flightId + + ", logContent='" + logContent + '\'' + + '}'; + } +} diff --git a/src/main/java/com/ruoyi/device/mapper/entity/PreCheckLogEntity.java b/src/main/java/com/ruoyi/device/mapper/entity/PreCheckLogEntity.java new file mode 100644 index 0000000..d0c4620 --- /dev/null +++ b/src/main/java/com/ruoyi/device/mapper/entity/PreCheckLogEntity.java @@ -0,0 +1,78 @@ +package com.ruoyi.device.mapper.entity; + +import com.ruoyi.common.core.web.domain.BaseEntity; + +/** + * 自检日志表实体对象 device_pre_check_log + * Mapper 层实体,对应数据库表 + * + * @author ruoyi + * @date 2026-02-25 + */ +public class PreCheckLogEntity extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 自检日志主键 */ + private Long logId; + + /** 关联飞行表ID */ + private Long flightId; + + /** 日志内容 (JSON字符串) */ + private String logContent; + + /** 是否成功 (true=成功, false=失败) */ + private Boolean success; + + public Long getLogId() + { + return logId; + } + + public void setLogId(Long logId) + { + this.logId = logId; + } + + public Long getFlightId() + { + return flightId; + } + + public void setFlightId(Long flightId) + { + this.flightId = flightId; + } + + public String getLogContent() + { + return logContent; + } + + public void setLogContent(String logContent) + { + this.logContent = logContent; + } + + public Boolean getSuccess() + { + return success; + } + + public void setSuccess(Boolean success) + { + this.success = success; + } + + @Override + public String toString() + { + return "PreCheckLogEntity{" + + "logId=" + logId + + ", flightId=" + flightId + + ", logContent='" + logContent + '\'' + + ", success=" + success + + '}'; + } +} diff --git a/src/main/java/com/ruoyi/device/service/FlightService.java b/src/main/java/com/ruoyi/device/service/FlightService.java new file mode 100644 index 0000000..ea93bf0 --- /dev/null +++ b/src/main/java/com/ruoyi/device/service/FlightService.java @@ -0,0 +1,96 @@ +package com.ruoyi.device.service; + +import com.ruoyi.device.mapper.entity.FlightEntity; + +import java.util.Map; + +/** + * 飞行服务接口 + * + * @author ruoyi + * @date 2026-02-25 + */ +public interface FlightService +{ + /** + * 获取或创建当前正在进行的飞行记录 + * 用于MQTT回调,如果最新记录已返航则创建新记录 + * + * @param deviceSn 设备SN号 + * @return 飞行记录 + */ + FlightEntity getOrCreateCurrentFlight(String deviceSn); + + /** + * 获取或创建飞行记录(通过messageID匹配) + * 如果存在相同messageID的飞行记录,返回该记录 + * 如果不存在或messageID不同,创建新记录 + * + * @param deviceSn 设备SN号 + * @param messageId 消息ID(对应flightIdExternal) + * @return 飞行记录 + */ + FlightEntity getOrCreateFlightByMessageId(String deviceSn, String messageId); + + /** + * 获取最新的飞行记录(包括已返航的) + * 用于WebSocket推送 + * + * @param deviceSn 设备SN号 + * @return 飞行记录 + */ + FlightEntity getLatestFlight(String deviceSn); + + /** + * 创建新的飞行记录 + * + * @param deviceSn 设备SN号 + * @return 飞行记录 + */ + FlightEntity createFlight(String deviceSn); + + /** + * 更新飞行ID(外部ID) + * + * @param flightId 飞行ID + * @param flightIdExternal 外部飞行ID + */ + void updateFlightIdExternal(Long flightId, String flightIdExternal); + + /** + * 更新飞行状态 + * + * @param flightId 飞行ID + * @param status 状态:自检中、飞行中、已返航 + */ + void updateFlightStatus(Long flightId, String status); + + /** + * 更新返航时间 + * + * @param flightId 飞行ID + */ + void updateReturnTime(Long flightId); + + /** + * 获取飞行记录和日志(用于WebSocket推送) + * + * @param deviceSn 设备SN号 + * @return 飞行记录和日志 + */ + Map getLatestFlightWithLogs(String deviceSn); + + /** + * 保存自检日志 + * + * @param logEntity 自检日志实体 + */ + void insertPreCheckLog(com.ruoyi.device.mapper.entity.PreCheckLogEntity logEntity); + + /** + * 保存飞行日志 + * + * @param logEntity 飞行日志实体 + */ + void insertFlightLog(com.ruoyi.device.mapper.entity.FlightLogEntity logEntity); +} diff --git a/src/main/java/com/ruoyi/device/service/impl/FlightEventCallback.java b/src/main/java/com/ruoyi/device/service/impl/FlightEventCallback.java new file mode 100644 index 0000000..6234d01 --- /dev/null +++ b/src/main/java/com/ruoyi/device/service/impl/FlightEventCallback.java @@ -0,0 +1,132 @@ +package com.ruoyi.device.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.device.domain.impl.tuohengmqtt.callback.IAirportFlyControlDataCallback; +import com.ruoyi.device.mapper.entity.FlightEntity; +import com.ruoyi.device.mapper.entity.FlightLogEntity; +import com.ruoyi.device.mapper.entity.PreCheckLogEntity; +import com.ruoyi.device.service.FlightService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class FlightEventCallback implements IAirportFlyControlDataCallback { + + @Autowired + private FlightService flightService; + + @Override + public void onAirportFlyControlData(String deviceSn, String payload, String topic) { + try { + + log.info("处理飞行事件: deviceSn={}, topic={}, payload={}", deviceSn, topic, payload); + + JSONObject data = JSONObject.parseObject(payload); + if (data == null) { + return; + } + + String msg = data.getString("msg"); + String messageID = data.getString("messageID"); + + String action = data.getString("action"); + if (action == null || action.isEmpty()) { + JSONObject dataObj = data.getJSONObject("data"); + if (dataObj != null) { + action = dataObj.getString("action"); + } + } + + FlightEntity flight; + if (messageID != null && !messageID.isEmpty()) { + flight = handleFlightIdExternal(deviceSn, messageID); + } else { + flight = flightService.getOrCreateCurrentFlight(deviceSn); + } + + if (msg != null && !msg.isEmpty()) { + if (msg.contains("自检")) { + handlePreCheckLog(deviceSn, msg, data, flight); + } else { + handleFlightLog(deviceSn, msg, flight); + } + handleFlightStatus(deviceSn, action, data, flight); + } + } catch (Exception e) { + log.error("处理飞行事件失败: deviceSn={}, topic={}, error={}", deviceSn, topic, e.getMessage(), e); + } + } + + private FlightEntity handleFlightIdExternal(String deviceSn, String messageId) { + if (messageId != null && !messageId.isEmpty()) { + return flightService.getOrCreateFlightByMessageId(deviceSn, messageId); + } else { + return flightService.getOrCreateCurrentFlight(deviceSn); + } + } + + private void handleFlightLog(String deviceSn, String message, FlightEntity flight) { + if (flight == null) { + log.warn("飞行记录为空,无法保存飞行日志: deviceSn={}, message={}", deviceSn, message); + return; + } + try { + FlightLogEntity logEntity = new FlightLogEntity(); + logEntity.setFlightId(flight.getFlightId()); + logEntity.setLogContent(message); + flightService.insertFlightLog(logEntity); + log.info("保存飞行日志: deviceSn={}, flightId={}, message={}", + deviceSn, flight.getFlightId(), message); + } catch (Exception e) { + log.error("保存飞行日志失败: deviceSn={}, message={}, error={}", + deviceSn, message, e.getMessage(), e); + } + } + + private void handlePreCheckLog(String deviceSn, String msg, JSONObject data, FlightEntity flight) { + if (flight == null) { + log.warn("飞行记录为空,无法保存自检日志: deviceSn={}, msg={}", deviceSn, msg); + return; + } + try { + Boolean success = msg.contains("通过") || msg.contains("成功"); + + PreCheckLogEntity logEntity = new PreCheckLogEntity(); + logEntity.setFlightId(flight.getFlightId()); + logEntity.setLogContent(msg); + logEntity.setSuccess(success); + flightService.insertPreCheckLog(logEntity); + + log.info("保存自检日志: deviceSn={}, flightId={}, msg={}, success={}", + deviceSn, flight.getFlightId(), msg, success); + } catch (Exception e) { + log.error("保存自检日志失败: deviceSn={}, msg={}, error={}", + deviceSn, msg, e.getMessage(), e); + } + } + + private void handleFlightStatus(String deviceSn, String action, JSONObject data, FlightEntity flight) { + if (flight == null) { + log.warn("飞行记录为空,无法更新状态: deviceSn={}, action={}", deviceSn, action); + return; + } + try { + String msg = data.getString("msg"); + String dataContent = data.getString("data"); + + if ((msg != null && msg.contains("起飞成功")) || (dataContent != null && dataContent.contains("起飞成功"))) { + flightService.updateFlightStatus(flight.getFlightId(), "飞行中"); + log.info("飞行状态更新: deviceSn={}, status=飞行中", deviceSn); + } else if ((msg != null && msg.contains("返航成功")) || (dataContent != null && dataContent.contains("返航成功")) || + (dataContent != null && dataContent.contains("任务飞行完成"))) { + flightService.updateFlightStatus(flight.getFlightId(), "已返航"); + log.info("飞行状态更新: deviceSn={}, status=已返航", deviceSn); + } + } catch (Exception e) { + log.error("更新飞行状态失败: deviceSn={}, action={}, error={}", + deviceSn, action, e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/ruoyi/device/service/impl/FlightLogCallback.java b/src/main/java/com/ruoyi/device/service/impl/FlightLogCallback.java new file mode 100644 index 0000000..11a0f35 --- /dev/null +++ b/src/main/java/com/ruoyi/device/service/impl/FlightLogCallback.java @@ -0,0 +1,89 @@ +package com.ruoyi.device.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.device.domain.impl.tuohengmqtt.callback.IDroneRealTimeCallback; +import com.ruoyi.device.domain.impl.tuohengmqtt.model.DroneRealTimeData; +import com.ruoyi.device.mapper.entity.FlightEntity; +import com.ruoyi.device.mapper.entity.FlightLogEntity; +import com.ruoyi.device.mapper.entity.PreCheckLogEntity; +import com.ruoyi.device.service.FlightService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 飞行日志回调处理器 + * 处理自检日志和飞行事件日志 + * + * @author ruoyi + * @date 2026-02-25 + */ +@Slf4j +@Component +public class FlightLogCallback implements IDroneRealTimeCallback { + + @Autowired + private FlightService flightService; + + @Override + public void onDroneRealTimeData(String deviceSn, DroneRealTimeData data) { + + if (data == null || data.getData() == null) { + return; + } + + log.info("处理实时消息: deviceSn={}, messageId={} data={}", deviceSn, data.getMessageID(),JSON.toJSONString(data.getData())); + + DroneRealTimeData.DroneInfo droneInfo = data.getData(); + + try { + if (droneInfo.getJiancha() != null && !droneInfo.getJiancha().isEmpty()) { + log.info("处理实时自检消息: deviceSn={}, messageId={}, checkBody={}", deviceSn, data.getMessageID(),data.getData().getJiancha()); + handlePreCheckLog(deviceSn, data.getMessageID(), droneInfo.getJiancha()); + } + } catch (Exception e) { + log.error("处理自检日志失败: deviceSn={}, error={}", deviceSn, e.getMessage(), e); + } + } + + private void handlePreCheckLog(String deviceSn, String messageID, String jianchaJson) { + try { + FlightEntity flight; + + if (messageID != null && !messageID.isEmpty()) { + flight = flightService.getOrCreateFlightByMessageId(deviceSn, messageID); + } else { + flight = flightService.getOrCreateCurrentFlight(deviceSn); + } + + JSONArray checkItems = JSON.parseArray(jianchaJson); + if (checkItems != null && !checkItems.isEmpty()) { + for (int i = 0; i < checkItems.size(); i++) { + JSONObject item = checkItems.getJSONObject(i); + PreCheckLogEntity logEntity = new PreCheckLogEntity(); + logEntity.setFlightId(flight.getFlightId()); + + String check = item.getString("check"); + String value = item.getString("value"); + Boolean result = item.getBoolean("result"); + + String statusText = result != null && result ? "自检成功" : "自检失败"; + String logContent = check + " " + value + " " + statusText; + + logEntity.setLogContent(logContent); + logEntity.setSuccess(result != null ? result : false); + flightService.insertPreCheckLog(logEntity); + } + log.info("保存自检日志: deviceSn={}, flightId={}, 检查项数量={}", + deviceSn, flight.getFlightId(), checkItems.size()); + } + } catch (Exception e) { + log.error("保存自检日志失败: deviceSn={}, jiancha={}, error={}", + deviceSn, jianchaJson, e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/ruoyi/device/service/impl/FlightServiceImpl.java b/src/main/java/com/ruoyi/device/service/impl/FlightServiceImpl.java new file mode 100644 index 0000000..00cfe4c --- /dev/null +++ b/src/main/java/com/ruoyi/device/service/impl/FlightServiceImpl.java @@ -0,0 +1,158 @@ +package com.ruoyi.device.service.impl; + +import com.ruoyi.device.mapper.FlightLogMapper; +import com.ruoyi.device.mapper.FlightMapper; +import com.ruoyi.device.mapper.PreCheckLogMapper; +import com.ruoyi.device.mapper.entity.FlightEntity; +import com.ruoyi.device.mapper.entity.FlightLogEntity; +import com.ruoyi.device.mapper.entity.PreCheckLogEntity; +import com.ruoyi.device.service.FlightService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 飞行服务实现类 + * + * @author ruoyi + * @date 2026-02-25 + */ +@Slf4j +@Service +public class FlightServiceImpl implements FlightService +{ + @Autowired + private FlightMapper flightMapper; + + @Autowired + private PreCheckLogMapper preCheckLogMapper; + + @Autowired + private FlightLogMapper flightLogMapper; + + @Override + public FlightEntity getOrCreateCurrentFlight(String deviceSn) { + FlightEntity flight = flightMapper.selectLatestFlightByDeviceSn(deviceSn); + + if (flight == null || "已返航".equals(flight.getStatus())) { + flight = createFlight(deviceSn); + } + + return flight; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public FlightEntity getOrCreateFlightByMessageId(String deviceSn, String messageId) { + FlightEntity flight = flightMapper.selectLatestFlightByDeviceSn(deviceSn); + + if (flight == null) { + flight = createFlight(deviceSn); + if (messageId != null && !messageId.isEmpty()) { + updateFlightIdExternal(flight.getFlightId(), messageId); + } + return flight; + } + + String existingFlightIdExternal = flight.getFlightIdExternal(); + if (existingFlightIdExternal == null || existingFlightIdExternal.isEmpty()) { + if (messageId != null && !messageId.isEmpty()) { + updateFlightIdExternal(flight.getFlightId(), messageId); + } + return flight; + } + + if (existingFlightIdExternal.equals(messageId)) { + return flight; + } + + flight = createFlight(deviceSn); + if (messageId != null && !messageId.isEmpty()) { + updateFlightIdExternal(flight.getFlightId(), messageId); + } + log.info("messageId不同,创建新飞行 - deviceSn={}, flightId={}, oldMessageId={}, newMessageId={}", + deviceSn, flight.getFlightId(), existingFlightIdExternal, messageId); + return flight; + } + + @Override + public FlightEntity getLatestFlight(String deviceSn) { + return flightMapper.selectLatestFlightByDeviceSn(deviceSn); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public FlightEntity createFlight(String deviceSn) { + FlightEntity flight = new FlightEntity(); + flight.setDeviceSn(deviceSn); + flight.setStatus("自检中"); + flightMapper.insertFlight(flight); + log.info("创建新的飞行记录: deviceSn={}, flightId={}", deviceSn, flight.getFlightId()); + return flight; + } + + @Override + public void updateFlightIdExternal(Long flightId, String flightIdExternal) { + FlightEntity flight = new FlightEntity(); + flight.setFlightId(flightId); + flight.setFlightIdExternal(flightIdExternal); + flightMapper.updateFlight(flight); + log.info("更新飞行ID: flightId={}, flightIdExternal={}", flightId, flightIdExternal); + } + + @Override + public void updateFlightStatus(Long flightId, String status) { + flightMapper.updateFlightStatus(flightId, status); + log.info("更新飞行状态: flightId={}, status={}", flightId, status); + + if ("已返航".equals(status)) { + flightMapper.updateReturnTime(flightId); + log.info("更新返航时间: flightId={}", flightId); + } + } + + @Override + public void updateReturnTime(Long flightId) { + flightMapper.updateReturnTime(flightId); + } + + @Override + public Map getLatestFlightWithLogs(String deviceSn) { + FlightEntity flight = flightMapper.selectLatestFlightByDeviceSn(deviceSn); + if (flight == null) { + return null; + } + + Map result = new HashMap<>(); + result.put("flightId", flight.getFlightId()); + result.put("deviceSn", flight.getDeviceSn()); + result.put("flightIdExternal", flight.getFlightIdExternal()); + result.put("status", flight.getStatus()); + result.put("returnTime", flight.getReturnTime()); + result.put("createTime", flight.getCreateTime()); + + List preCheckLogs = preCheckLogMapper.selectPreCheckLogListByFlightId(flight.getFlightId()); + result.put("preCheckLogs", preCheckLogs); + + List flightLogs = flightLogMapper.selectFlightLogListByFlightId(flight.getFlightId()); + result.put("flightLogs", flightLogs); + + return result; + } + + @Override + public void insertPreCheckLog(PreCheckLogEntity logEntity) { + preCheckLogMapper.insertPreCheckLog(logEntity); + } + + @Override + public void insertFlightLog(FlightLogEntity logEntity) { + flightLogMapper.insertFlightLog(logEntity); + } +} diff --git a/src/main/java/com/ruoyi/device/service/impl/FlightStateChangeListener.java b/src/main/java/com/ruoyi/device/service/impl/FlightStateChangeListener.java new file mode 100644 index 0000000..8ac1f0c --- /dev/null +++ b/src/main/java/com/ruoyi/device/service/impl/FlightStateChangeListener.java @@ -0,0 +1,78 @@ +package com.ruoyi.device.service.impl; + +import com.ruoyi.device.domain.impl.machine.state.DroneState; +import com.ruoyi.device.domain.impl.machine.state.MachineStates; +import com.ruoyi.device.domain.impl.machine.statemachine.StateChangeListener; +import com.ruoyi.device.mapper.entity.FlightEntity; +import com.ruoyi.device.service.FlightService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +/** + * 飞行状态变化监听器 + * 监听状态机的状态变化,更新飞行表中的状态 + * + * @author ruoyi + * @date 2026-02-25 + */ +@Slf4j +@Component +public class FlightStateChangeListener implements StateChangeListener { + + @Autowired + private FlightService flightService; + + @Autowired + private com.ruoyi.device.domain.impl.machine.statemachine.MachineStateManager stateManager; + + @EventListener(ApplicationReadyEvent.class) + public void onApplicationReady() { + stateManager.registerStateChangeListener("flight-state-changer", this); + log.info("飞行状态变化监听器已注册"); + } + + @Override + public void onStateChange(String sn, MachineStates newStates) { + try { + DroneState droneState = newStates.getDroneState(); + if (droneState == DroneState.UNKNOWN) { + return; + } + + FlightEntity flight = flightService.getOrCreateCurrentFlight(sn); + if (flight == null) { + return; + } + + String currentStatus = flight.getStatus(); + String newStatus = mapDroneStateToFlightStatus(droneState); + + if (!currentStatus.equals(newStatus)) { + flightService.updateFlightStatus(flight.getFlightId(), newStatus); + log.info("状态变化更新飞行状态: sn={}, droneState={}, flightStatus={}", + sn, droneState, newStatus); + } + } catch (Exception e) { + log.error("状态变化监听器处理失败: sn={}, error={}", sn, e.getMessage(), e); + } + } + + private String mapDroneStateToFlightStatus(DroneState droneState) { + switch (droneState) { + case ONLINE: + return "自检中"; + case FLYING: + return "飞行中"; + case ARRIVED: + return "已返航"; + case RETURNING: + return "已返航"; + case UNKNOWN: + default: + return "自检中"; + } + } +} diff --git a/src/main/java/com/ruoyi/device/service/impl/TuohengService.java b/src/main/java/com/ruoyi/device/service/impl/TuohengService.java index 811da82..5a746e0 100644 --- a/src/main/java/com/ruoyi/device/service/impl/TuohengService.java +++ b/src/main/java/com/ruoyi/device/service/impl/TuohengService.java @@ -66,6 +66,12 @@ public class TuohengService { @Autowired private MqttCallbackRegistry mqttCallbackRegistry; + @Autowired + private FlightEventCallback flightEventCallback; + + @Autowired + private FlightLogCallback flightLogCallback; + private final ObjectMapper objectMapper = new ObjectMapper(); /** @@ -99,7 +105,7 @@ public class TuohengService { TuohengMqttMessageHandler handler = clientManager.getHandler(); // 设置 MqttCallbackRegistry 到 handler(用于指令回调) - handler.setMqttCallbackRegistry(mqttCallbackRegistry); + handler.setMachineCallBackRegistry(mqttCallbackRegistry); Map mapping = loadAirportDroneMapping(); @@ -183,6 +189,10 @@ public class TuohengService { } }); + handler.registerDroneRealTimeCallback(flightLogCallback); + + handler.registerAirportFlyControlDataCallback(flightEventCallback); + log.info("TuohengService 初始化完成,已注册所有回调"); } diff --git a/src/main/java/com/ruoyi/device/websocket/FlightLogWebSocket.java b/src/main/java/com/ruoyi/device/websocket/FlightLogWebSocket.java new file mode 100644 index 0000000..2b2652c --- /dev/null +++ b/src/main/java/com/ruoyi/device/websocket/FlightLogWebSocket.java @@ -0,0 +1,100 @@ +package com.ruoyi.device.websocket; + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.device.mapper.entity.FlightLogEntity; +import com.ruoyi.device.service.FlightService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import jakarta.websocket.*; +import jakarta.websocket.server.PathParam; +import jakarta.websocket.server.ServerEndpoint; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * 飞行日志WebSocket + * + * @author ruoyi + * @date 2026-02-25 + */ +@Slf4j +@Component +@ServerEndpoint("/websocket/flightLog/{deviceSn}") +public class FlightLogWebSocket { + + private Session session; + + private String deviceSn; + + private static final CopyOnWriteArraySet sessions = new CopyOnWriteArraySet<>(); + + private static final Map sessionMap = new ConcurrentHashMap<>(); + + @Autowired + private FlightService flightService; + + @OnOpen + public void onOpen(Session session, @PathParam("deviceSn") String deviceSn) { + this.session = session; + this.deviceSn = deviceSn; + sessions.add(this); + sessionMap.put(session.getId(), this); + log.info("飞行日志WebSocket连接建立: sessionId={}, deviceSn={}", session.getId(), deviceSn); + } + + @OnClose + public void onClose() { + sessions.remove(this); + if (session != null) { + sessionMap.remove(session.getId()); + log.info("飞行日志WebSocket连接关闭: sessionId={}, deviceSn={}", session.getId(), deviceSn); + } else { + log.info("飞行日志WebSocket连接关闭: session为null"); + } + } + + @OnMessage + public void onMessage(String message) { + log.info("收到飞行日志WebSocket消息: sessionId={}, message={}", session.getId(), message); + } + + @OnError + public void onError(Session session, Throwable error) { + log.error("飞行日志WebSocket错误: sessionId={}, deviceSn={}, error={}", + session.getId(), deviceSn, error.getMessage(), error); + } + + private void sendMessage(String message) { + try { + if (session != null && session.isOpen()) { + session.getBasicRemote().sendText(message); + } + } catch (Exception e) { + log.error("发送飞行日志WebSocket消息失败: deviceSn={}, error={}", deviceSn, e.getMessage(), e); + } + } + + @Scheduled(fixedRate = 3000) + public void broadcast() { + for (FlightLogWebSocket ws : sessions) { + Map flightData = flightService.getLatestFlightWithLogs(ws.deviceSn); + if (flightData == null) { + continue; + } + + String status = (String) flightData.get("status"); + List logs = (List) flightData.get("flightLogs"); + + Map response = new ConcurrentHashMap<>(); + response.put("status", status); + response.put("logs", logs); + + ws.sendMessage(JSON.toJSONString(response)); + } + } +} diff --git a/src/main/java/com/ruoyi/device/websocket/PreCheckLogWebSocket.java b/src/main/java/com/ruoyi/device/websocket/PreCheckLogWebSocket.java new file mode 100644 index 0000000..2c4ed25 --- /dev/null +++ b/src/main/java/com/ruoyi/device/websocket/PreCheckLogWebSocket.java @@ -0,0 +1,101 @@ +package com.ruoyi.device.websocket; + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.device.mapper.entity.PreCheckLogEntity; +import com.ruoyi.device.service.FlightService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import jakarta.websocket.*; +import jakarta.websocket.server.PathParam; +import jakarta.websocket.server.ServerEndpoint; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * 自检日志WebSocket + * + * @author ruoyi + * @date 2026-02-25 + */ +@Slf4j +@Component +@ServerEndpoint("/websocket/preCheckLog/{deviceSn}") +public class PreCheckLogWebSocket { + + private Session session; + + private String deviceSn; + + private static final CopyOnWriteArraySet sessions = new CopyOnWriteArraySet<>(); + + private static final Map sessionMap = new ConcurrentHashMap<>(); + + @Autowired + private FlightService flightService; + + @OnOpen + public void onOpen(Session session, @PathParam("deviceSn") String deviceSn) { + this.session = session; + this.deviceSn = deviceSn; + sessions.add(this); + sessionMap.put(session.getId(), this); + log.info("自检日志WebSocket连接建立: sessionId={}, deviceSn={}", session.getId(), deviceSn); + } + + @OnClose + public void onClose() { + sessions.remove(this); + if (session != null) { + sessionMap.remove(session.getId()); + log.info("自检日志WebSocket连接关闭: sessionId={}, deviceSn={}", session.getId(), deviceSn); + } else { + log.info("自检日志WebSocket连接关闭: session为null"); + } + } + + @OnMessage + public void onMessage(String message) { + log.info("收到自检日志WebSocket消息: sessionId={}, message={}", session.getId(), message); + } + + @OnError + public void onError(Session session, Throwable error) { + log.error("自检日志WebSocket错误: sessionId={}, deviceSn={}, error={}", + session.getId(), deviceSn, error.getMessage(), error); + } + + private void sendMessage(String message) { + try { + if (session != null && session.isOpen()) { + session.getBasicRemote().sendText(message); + } + } catch (Exception e) { + log.error("发送自检日志WebSocket消息失败: deviceSn={}, error={}", deviceSn, e.getMessage(), e); + } + } + + @Scheduled(fixedRate = 3000) + public void broadcast() { + for (PreCheckLogWebSocket ws : sessions) { + Map flightData = flightService.getLatestFlightWithLogs(ws.deviceSn); + if (flightData == null) { + continue; + } + + String status = (String) flightData.get("status"); + List logs = (List) flightData.get("preCheckLogs"); + + Map response = new ConcurrentHashMap<>(); + response.put("status", status); + response.put("logs", logs); + + ws.sendMessage(JSON.toJSONString(response)); + } + } +} diff --git a/src/main/resources/db/migration/V6__Create_flight_log_tables.sql b/src/main/resources/db/migration/V6__Create_flight_log_tables.sql new file mode 100644 index 0000000..1afe1b0 --- /dev/null +++ b/src/main/resources/db/migration/V6__Create_flight_log_tables.sql @@ -0,0 +1,48 @@ +-- ============================================================ +-- Flyway Migration Script +-- Version: V3 +-- Description: Create flight log tables (flight, pre_check_log, flight_log) +-- Author: ruoyi +-- Date: 2026-02-25 +-- ============================================================ + +-- 创建飞行表 +CREATE TABLE IF NOT EXISTS device_flight ( + flight_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '飞行主键', + device_sn VARCHAR(100) NOT NULL COMMENT '无人机SN', + flight_id_external VARCHAR(100) COMMENT '外部飞行ID (来自MQTT的taskId)', + status VARCHAR(20) COMMENT '状态:自检中、飞行中、已返航', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + return_time DATETIME COMMENT '返航时间', + create_by VARCHAR(64) COMMENT '创建者', + update_by VARCHAR(64) COMMENT '更新者', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + remark VARCHAR(500) COMMENT '备注', + PRIMARY KEY (flight_id), + KEY idx_flight_device_sn (device_sn), + KEY idx_flight_id_external (flight_id_external), + KEY idx_flight_status (status), + KEY idx_flight_create_time (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='飞行表'; + +-- 创建自检日志表 +CREATE TABLE IF NOT EXISTS device_pre_check_log ( + log_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '自检日志主键', + flight_id BIGINT NOT NULL COMMENT '关联飞行表ID', + log_content TEXT COMMENT '日志内容 (JSON字符串)', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (log_id), + KEY idx_pre_check_flight_id (flight_id), + KEY idx_pre_check_create_time (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='自检日志表'; + +-- 创建飞行日志表 +CREATE TABLE IF NOT EXISTS device_flight_log ( + log_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '飞行日志主键', + flight_id BIGINT NOT NULL COMMENT '关联飞行表ID', + log_content TEXT COMMENT '日志内容', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (log_id), + KEY idx_flight_log_flight_id (flight_id), + KEY idx_flight_log_create_time (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='飞行日志表'; diff --git a/src/main/resources/db/migration/V7__Add_success_field_to_pre_check_log.sql b/src/main/resources/db/migration/V7__Add_success_field_to_pre_check_log.sql new file mode 100644 index 0000000..3e8469e --- /dev/null +++ b/src/main/resources/db/migration/V7__Add_success_field_to_pre_check_log.sql @@ -0,0 +1,11 @@ +-- ============================================================ +-- Flyway Migration Script +-- Version: V6 +-- Description: Add success field to device_pre_check_log table +-- Author: ruoyi +-- Date: 2026-02-25 +-- ============================================================ + +ALTER TABLE device_pre_check_log +ADD COLUMN success TINYINT(1) DEFAULT 0 COMMENT '是否成功 (1=成功, 0=失败)' +AFTER log_content; diff --git a/src/main/resources/mapper/device/FlightLogMapper.xml b/src/main/resources/mapper/device/FlightLogMapper.xml new file mode 100644 index 0000000..efa3801 --- /dev/null +++ b/src/main/resources/mapper/device/FlightLogMapper.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + select log_id, flight_id, log_content, + create_by, create_time, update_by, update_time, remark + from device_flight_log + + + + + + + + insert into device_flight_log + + flight_id, + log_content, + create_by, + remark, + create_time + + + #{flightId}, + #{logContent}, + #{createBy}, + #{remark}, + now() + + + + diff --git a/src/main/resources/mapper/device/FlightMapper.xml b/src/main/resources/mapper/device/FlightMapper.xml new file mode 100644 index 0000000..8e551b3 --- /dev/null +++ b/src/main/resources/mapper/device/FlightMapper.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + select flight_id, device_sn, flight_id_external, status, return_time, + create_by, create_time, update_by, update_time, remark + from device_flight + + + + + + + + + + insert into device_flight + + device_sn, + flight_id_external, + status, + return_time, + create_by, + remark, + create_time + + + #{deviceSn}, + #{flightIdExternal}, + #{status}, + #{returnTime}, + #{createBy}, + #{remark}, + now() + + + + + update device_flight + + flight_id_external = #{flightIdExternal}, + status = #{status}, + return_time = #{returnTime}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = now() + + where flight_id = #{flightId} + + + + update device_flight + set status = #{status}, + update_time = now() + where flight_id = #{flightId} + + + + update device_flight + set return_time = now(), + update_time = now() + where flight_id = #{flightId} + + + diff --git a/src/main/resources/mapper/device/PreCheckLogMapper.xml b/src/main/resources/mapper/device/PreCheckLogMapper.xml new file mode 100644 index 0000000..3b5af82 --- /dev/null +++ b/src/main/resources/mapper/device/PreCheckLogMapper.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + select log_id, flight_id, log_content, success, + create_by, create_time, update_by, update_time, remark + from device_pre_check_log + + + + + + + + insert into device_pre_check_log + + flight_id, + log_content, + success, + create_by, + remark, + create_time + + + #{flightId}, + #{logContent}, + #{success}, + #{createBy}, + #{remark}, + now() + + + + + insert into device_pre_check_log (flight_id, log_content, success, create_time) + values + + (#{item.flightId}, #{item.logContent}, #{item.success}, now()) + + + +