From 51fe7cf816df68883dda35c69aae992aa52a934a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E5=B0=8F=E4=BA=91?= Date: Mon, 15 Dec 2025 19:57:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=8A=B6=E6=80=81=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../machine/config/AirportMachineConfig.java | 40 +++++- .../machine/config/CoverMachineConfig.java | 46 +++++- .../machine/demo/MultiPlatformDemo.java | 2 +- .../manager/AbstractAirportSystemManager.java | 136 ++++++++++++++++++ .../machine/manager/AirportSystemManager.java | 14 ++ .../impl/dji/DjiAirportSystemManager.java | 8 ++ .../status/machine/redis/RedisStateStore.java | 52 +++++++ .../service/AirportMachineService.java | 12 +- .../machine/service/CoverMachineService.java | 12 +- .../status/machine/status/AirportState.java | 5 + .../status/machine/status/CoverState.java | 10 +- 11 files changed, 327 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/tuoheng/status/machine/redis/RedisStateStore.java diff --git a/src/main/java/com/tuoheng/status/machine/config/AirportMachineConfig.java b/src/main/java/com/tuoheng/status/machine/config/AirportMachineConfig.java index b0e8e8f..b381b61 100644 --- a/src/main/java/com/tuoheng/status/machine/config/AirportMachineConfig.java +++ b/src/main/java/com/tuoheng/status/machine/config/AirportMachineConfig.java @@ -70,8 +70,9 @@ public class AirportMachineConfig { private void configureStates(StateMachineBuilder.Builder builder) throws Exception { builder.configureStates() .withStates() - .initial(AirportState.OFFLINE) + .initial(AirportState.UNKNOWN) .states(EnumSet.of( + AirportState.UNKNOWN, AirportState.OFFLINE, AirportState.ONLINE, AirportState.REBOOTING @@ -90,6 +91,43 @@ public class AirportMachineConfig { StateMachineBuilder.Builder builder, AirportPlatformStrategy strategy) throws Exception { builder.configureTransitions() + // ========== 从 UNKNOWN 到所有状态的转换(服务器重启后状态同步) ========== + // UNKNOWN -> OFFLINE + .withExternal() + .source(AirportState.UNKNOWN) + .target(AirportState.OFFLINE) + .event(AirportEvent.AIRPORT_OFFLINE) + .and() + + // UNKNOWN -> ONLINE(STANDBY) + .withExternal() + .source(AirportState.UNKNOWN) + .target(AirportState.ONLINE) + .event(AirportEvent.AIRPORT_ONLINE) + .and() + + // UNKNOWN -> STANDBY + .withExternal() + .source(AirportState.UNKNOWN) + .target(AirportState.STANDBY) + .event(AirportEvent.AIRPORT_ONLINE) + .and() + + // UNKNOWN -> DEBUG_MODE + .withExternal() + .source(AirportState.UNKNOWN) + .target(AirportState.DEBUG_MODE) + .event(AirportEvent.DEBUG_MODE_OPEN) + .and() + + // UNKNOWN -> REBOOTING + .withExternal() + .source(AirportState.UNKNOWN) + .target(AirportState.REBOOTING) + .event(AirportEvent.AIRPORT_REBOOT) + .and() + + // ========== 正常状态转换(带 Guard 和 Action) ========== // OFFLINE -> ONLINE(STANDBY) .withExternal() .source(AirportState.OFFLINE) diff --git a/src/main/java/com/tuoheng/status/machine/config/CoverMachineConfig.java b/src/main/java/com/tuoheng/status/machine/config/CoverMachineConfig.java index a502edd..d1619f3 100644 --- a/src/main/java/com/tuoheng/status/machine/config/CoverMachineConfig.java +++ b/src/main/java/com/tuoheng/status/machine/config/CoverMachineConfig.java @@ -70,7 +70,7 @@ public class CoverMachineConfig { private void configureCoverStates(StateMachineBuilder.Builder builder) throws Exception { builder.configureStates() .withStates() - .initial(CoverState.CLOSED) + .initial(CoverState.UNKNOWN) .states(EnumSet.allOf(CoverState.class)); } @@ -78,6 +78,50 @@ public class CoverMachineConfig { StateMachineBuilder.Builder builder, CoverPlatformStrategy strategy) throws Exception { builder.configureTransitions() + // ========== 从 UNKNOWN 到所有状态的转换(服务器重启后状态同步) ========== + // UNKNOWN -> CLOSED + .withExternal() + .source(CoverState.UNKNOWN) + .target(CoverState.CLOSED) + .event(CoverEvent.CLOSED) + .and() + + // UNKNOWN -> OPENING + .withExternal() + .source(CoverState.UNKNOWN) + .target(CoverState.OPENING) + .event(CoverEvent.OPEN) + .and() + + // UNKNOWN -> OPENED + .withExternal() + .source(CoverState.UNKNOWN) + .target(CoverState.OPENED) + .event(CoverEvent.OPENED) + .and() + + // UNKNOWN -> CLOSING + .withExternal() + .source(CoverState.UNKNOWN) + .target(CoverState.CLOSING) + .event(CoverEvent.CLOSE) + .and() + + // UNKNOWN -> HALF_OPEN + .withExternal() + .source(CoverState.UNKNOWN) + .target(CoverState.HALF_OPEN) + .event(CoverEvent.OPENED) + .and() + + // UNKNOWN -> ERROR + .withExternal() + .source(CoverState.UNKNOWN) + .target(CoverState.ERROR) + .event(CoverEvent.ERROR) + .and() + + // ========== 正常状态转换(带 Guard 和 Action) ========== // CLOSED -> OPENING .withExternal() .source(CoverState.CLOSED) diff --git a/src/main/java/com/tuoheng/status/machine/demo/MultiPlatformDemo.java b/src/main/java/com/tuoheng/status/machine/demo/MultiPlatformDemo.java index 0161c95..846049d 100644 --- a/src/main/java/com/tuoheng/status/machine/demo/MultiPlatformDemo.java +++ b/src/main/java/com/tuoheng/status/machine/demo/MultiPlatformDemo.java @@ -21,7 +21,7 @@ public class MultiPlatformDemo { // 获取必要的Bean PlatformStrategyFactory strategyFactory = context.getBean(PlatformStrategyFactory.class); - AirportPlatformRepository repository = context.getBean(AirportPlatformRepository.class); + System.out.println("\n========== DJI 机巢系统演示开始 ==========\n"); diff --git a/src/main/java/com/tuoheng/status/machine/manager/AbstractAirportSystemManager.java b/src/main/java/com/tuoheng/status/machine/manager/AbstractAirportSystemManager.java index 6ee0b6c..050ea1e 100644 --- a/src/main/java/com/tuoheng/status/machine/manager/AbstractAirportSystemManager.java +++ b/src/main/java/com/tuoheng/status/machine/manager/AbstractAirportSystemManager.java @@ -1,8 +1,12 @@ package com.tuoheng.status.machine.manager; +import com.tuoheng.status.machine.events.AirportEvent; +import com.tuoheng.status.machine.events.CoverEvent; import com.tuoheng.status.machine.platform.PlatformType; import com.tuoheng.status.machine.service.AirportMachineService; import com.tuoheng.status.machine.service.CoverMachineService; +import com.tuoheng.status.machine.status.AirportState; +import com.tuoheng.status.machine.status.CoverState; import org.springframework.beans.factory.annotation.Autowired; /** @@ -22,5 +26,137 @@ public abstract class AbstractAirportSystemManager implements AirportSystemManag @Autowired protected CoverMachineService coverService; + + + public boolean sendEvent(String airportSn, AirportEvent event) { + return airportService.sendEvent(airportSn, event); + } + + public boolean sendEvent(String airportSn, CoverEvent event) { + return coverService.sendEvent(airportSn, event); + } + + /** + * 同步机巢状态 + * 仅在当前状态为 UNKNOWN 时才同步到目标状态 + * + * @param airportSn 机巢序列号 + * @param targetState 目标状态 + * @return 是否同步成功 + */ + public boolean syncAirportState(String airportSn, AirportState targetState) { + AirportState currentState = airportService.getCurrentState(airportSn); + + if (currentState == null) { + System.out.println(String.format("同步机巢状态失败 - 机巢: %s, 状态机不存在", airportSn)); + return false; + } + + if (currentState != AirportState.UNKNOWN) { + System.out.println(String.format("同步机巢状态跳过 - 机巢: %s, 当前状态: %s (非UNKNOWN状态,无需同步)", + airportSn, currentState)); + return false; + } + + // 根据目标状态发送相应的事件 + AirportEvent event = getAirportEventForState(targetState); + if (event == null) { + System.out.println(String.format("同步机巢状态失败 - 机巢: %s, 无法为目标状态 %s 找到对应事件", + airportSn, targetState)); + return false; + } + + boolean result = airportService.sendEvent(airportSn, event); + if (result) { + System.out.println(String.format("同步机巢状态成功 - 机巢: %s, 从 UNKNOWN 同步到 %s", + airportSn, targetState)); + } + return result; + } + + /** + * 同步舱门状态 + * 仅在当前状态为 UNKNOWN 时才同步到目标状态 + * + * @param airportSn 机巢序列号 + * @param targetState 目标状态 + * @return 是否同步成功 + */ + public boolean syncCoverState(String airportSn, CoverState targetState) { + CoverState currentState = coverService.getCurrentState(airportSn); + + if (currentState == null) { + System.out.println(String.format("同步舱门状态失败 - 机巢: %s, 状态机不存在", airportSn)); + return false; + } + + if (currentState != CoverState.UNKNOWN) { + System.out.println(String.format("同步舱门状态跳过 - 机巢: %s, 当前状态: %s (非UNKNOWN状态,无需同步)", + airportSn, currentState)); + return false; + } + + // 根据目标状态发送相应的事件 + CoverEvent event = getCoverEventForState(targetState); + if (event == null) { + System.out.println(String.format("同步舱门状态失败 - 机巢: %s, 无法为目标状态 %s 找到对应事件", + airportSn, targetState)); + return false; + } + + boolean result = coverService.sendEvent(airportSn, event); + if (result) { + System.out.println(String.format("同步舱门状态成功 - 机巢: %s, 从 UNKNOWN 同步到 %s", + airportSn, targetState)); + } + return result; + } + + /** + * 根据目标状态获取对应的机巢事件 + * 子类可以重写此方法以提供自定义的状态到事件的映射 + * + * @param targetState 目标状态 + * @return 对应的事件,如果没有对应事件则返回null + */ + protected AirportEvent getAirportEventForState(AirportState targetState) { + switch (targetState) { + case ONLINE: + case STANDBY: + return AirportEvent.AIRPORT_ONLINE; + case OFFLINE: + return AirportEvent.AIRPORT_OFFLINE; + case DEBUG_MODE: + return AirportEvent.DEBUG_MODE_OPEN; + case REBOOTING: + return AirportEvent.AIRPORT_REBOOT; + default: + return null; + } + } + + /** + * 根据目标状态获取对应的舱门事件 + * 子类可以重写此方法以提供自定义的状态到事件的映射 + * + * @param targetState 目标状态 + * @return 对应的事件,如果没有对应事件则返回null + */ + protected CoverEvent getCoverEventForState(CoverState targetState) { + switch (targetState) { + case CLOSED: + return CoverEvent.CLOSED; + case OPENED: + return CoverEvent.OPENED; + case OPENING: + return CoverEvent.OPEN; + case CLOSING: + return CoverEvent.CLOSE; + case ERROR: + return CoverEvent.ERROR; + default: + return null; + } + } } diff --git a/src/main/java/com/tuoheng/status/machine/manager/AirportSystemManager.java b/src/main/java/com/tuoheng/status/machine/manager/AirportSystemManager.java index 2410666..5d11c63 100644 --- a/src/main/java/com/tuoheng/status/machine/manager/AirportSystemManager.java +++ b/src/main/java/com/tuoheng/status/machine/manager/AirportSystemManager.java @@ -1,5 +1,7 @@ package com.tuoheng.status.machine.manager; +import com.tuoheng.status.machine.events.AirportEvent; +import com.tuoheng.status.machine.events.CoverEvent; import com.tuoheng.status.machine.platform.PlatformType; import com.tuoheng.status.machine.status.AirportState; import com.tuoheng.status.machine.status.CoverState; @@ -15,6 +17,18 @@ public interface AirportSystemManager { */ PlatformType getPlatformType(); + public boolean sendEvent(String airportSn, AirportEvent event); + + public boolean sendEvent(String airportSn, CoverEvent event); + + public boolean syncAirportState(String airportSn, AirportState targetState); + + public boolean syncCoverState(String airportSn, CoverState targetState); + /** + * 有心跳的时候调用 + * @param airportSn + * @return + */ boolean airportOnline(String airportSn); boolean airportOffline(String airportSn); diff --git a/src/main/java/com/tuoheng/status/machine/platform/impl/dji/DjiAirportSystemManager.java b/src/main/java/com/tuoheng/status/machine/platform/impl/dji/DjiAirportSystemManager.java index 6b61865..7c2ee17 100644 --- a/src/main/java/com/tuoheng/status/machine/platform/impl/dji/DjiAirportSystemManager.java +++ b/src/main/java/com/tuoheng/status/machine/platform/impl/dji/DjiAirportSystemManager.java @@ -20,11 +20,19 @@ public class DjiAirportSystemManager extends AbstractAirportSystemManager { return PlatformType.DJI; } + /** + * @param airportSn + * @return + */ @Override public boolean airportOnline(String airportSn) { return airportService.sendEvent(airportSn, AirportEvent.AIRPORT_ONLINE); } + /** + * @param airportSn + * @return + */ @Override public boolean airportOffline(String airportSn) { return airportService.sendEvent(airportSn, AirportEvent.AIRPORT_OFFLINE); diff --git a/src/main/java/com/tuoheng/status/machine/redis/RedisStateStore.java b/src/main/java/com/tuoheng/status/machine/redis/RedisStateStore.java new file mode 100644 index 0000000..679a0b8 --- /dev/null +++ b/src/main/java/com/tuoheng/status/machine/redis/RedisStateStore.java @@ -0,0 +1,52 @@ +package com.tuoheng.status.machine.redis; + +import com.tuoheng.status.machine.status.AirportState; +import com.tuoheng.status.machine.status.CoverState; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 使用 Redis 记录和恢复机巢/舱门状态的存储组件。 + * + * 当前实现采用内存 Map 占位,便于无 Redis 环境下直接运行。 + * 如果接入真正的 Redis,只需将存取逻辑替换为 RedisTemplate 等实现。 + */ +@Component +public class RedisStateStore { + + private final Map airportStateMap = new ConcurrentHashMap<>(); + private final Map coverStateMap = new ConcurrentHashMap<>(); + + public void saveAirportState(String airportSn, AirportState state) { + if (airportSn != null && state != null) { + airportStateMap.put(airportSn, state); + } + } + + public void saveCoverState(String airportSn, CoverState state) { + if (airportSn != null && state != null) { + coverStateMap.put(airportSn, state); + } + } + + public Optional loadAirportState(String airportSn) { + return Optional.ofNullable(airportStateMap.get(airportSn)); + } + + public Optional loadCoverState(String airportSn) { + return Optional.ofNullable(coverStateMap.get(airportSn)); + } + + public Set allAirportIds() { + // 合并两张表的 key,防止有只存机场或只存舱门的情况 + Set ids = ConcurrentHashMap.newKeySet(); + ids.addAll(airportStateMap.keySet()); + ids.addAll(coverStateMap.keySet()); + return ids; + } +} + diff --git a/src/main/java/com/tuoheng/status/machine/service/AirportMachineService.java b/src/main/java/com/tuoheng/status/machine/service/AirportMachineService.java index a55c533..b69871e 100644 --- a/src/main/java/com/tuoheng/status/machine/service/AirportMachineService.java +++ b/src/main/java/com/tuoheng/status/machine/service/AirportMachineService.java @@ -1,10 +1,12 @@ package com.tuoheng.status.machine.service; import com.tuoheng.status.machine.events.AirportEvent; +import com.tuoheng.status.machine.redis.RedisStateStore; import com.tuoheng.status.machine.status.AirportState; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.config.StateMachineFactory; +import org.springframework.statemachine.support.DefaultStateMachineContext; import org.springframework.stereotype.Component; import java.util.Map; @@ -20,6 +22,9 @@ public class AirportMachineService { @Autowired StateMachineFactory stateMachineFactory; + @Autowired + private RedisStateStore redisStateStore; + /** * 存储所有机巢的状态机实例 * Key: 机巢ID (airportSn) @@ -37,8 +42,11 @@ public class AirportMachineService { public StateMachine getOrCreateStateMachine(String airportSn) { return stateMachineMap.computeIfAbsent(airportSn, id -> { StateMachine stateMachine = stateMachineFactory.getStateMachine(id); + // 服务器重启后,状态机初始化为 UNKNOWN 状态 + // 不从 Redis 恢复旧状态,等待第一次心跳同步真实状态 + // 这样可以避免服务器重启期间丢失心跳导致的状态不一致问题 stateMachine.start(); - System.out.println("创建并启动状态机: " + id); + System.out.println(String.format("创建并启动状态机: %s, 初始状态: UNKNOWN (等待心跳同步)", id)); return stateMachine; }); } @@ -103,6 +111,8 @@ public class AirportMachineService { boolean result = stateMachine.sendEvent(event); if (result) { + // 持久化最新状态 + redisStateStore.saveAirportState(airportSn, stateMachine.getState().getId()); System.out.println(String.format("事件发送成功 - 机巢: %s, 事件: %s, 当前状态: %s", airportSn, event, getCurrentStates(airportSn))); } else { diff --git a/src/main/java/com/tuoheng/status/machine/service/CoverMachineService.java b/src/main/java/com/tuoheng/status/machine/service/CoverMachineService.java index 2da16a3..44c9195 100644 --- a/src/main/java/com/tuoheng/status/machine/service/CoverMachineService.java +++ b/src/main/java/com/tuoheng/status/machine/service/CoverMachineService.java @@ -1,10 +1,12 @@ package com.tuoheng.status.machine.service; import com.tuoheng.status.machine.events.CoverEvent; +import com.tuoheng.status.machine.redis.RedisStateStore; import com.tuoheng.status.machine.status.CoverState; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.config.StateMachineFactory; +import org.springframework.statemachine.support.DefaultStateMachineContext; import org.springframework.stereotype.Component; import java.util.Map; @@ -19,13 +21,19 @@ public class CoverMachineService { @Autowired StateMachineFactory coverStateMachineFactory; + @Autowired + private RedisStateStore redisStateStore; + private final Map> stateMachineMap = new ConcurrentHashMap<>(); public StateMachine getOrCreateStateMachine(String airportSn) { return stateMachineMap.computeIfAbsent(airportSn, id -> { StateMachine stateMachine = coverStateMachineFactory.getStateMachine(id); + // 服务器重启后,状态机初始化为 UNKNOWN 状态 + // 不从 Redis 恢复旧状态,等待第一次心跳同步真实状态 + // 这样可以避免服务器重启期间丢失心跳导致的状态不一致问题 stateMachine.start(); - System.out.println("创建并启动舱门状态机: " + id); + System.out.println(String.format("创建并启动舱门状态机: %s, 初始状态: UNKNOWN (等待心跳同步)", id)); return stateMachine; }); } @@ -43,6 +51,8 @@ public class CoverMachineService { boolean result = stateMachine.sendEvent(event); if (result) { + // 持久化最新状态 + redisStateStore.saveCoverState(airportSn, stateMachine.getState().getId()); System.out.println(String.format("舱门事件发送成功 - 机巢: %s, 事件: %s, 当前状态: %s", airportSn, event, getCurrentState(airportSn))); } else { diff --git a/src/main/java/com/tuoheng/status/machine/status/AirportState.java b/src/main/java/com/tuoheng/status/machine/status/AirportState.java index f517446..1f1316f 100644 --- a/src/main/java/com/tuoheng/status/machine/status/AirportState.java +++ b/src/main/java/com/tuoheng/status/machine/status/AirportState.java @@ -4,6 +4,11 @@ package com.tuoheng.status.machine.status; * 机巢状态枚举(简化版 - 舱门状态已分离) */ public enum AirportState { + /** + * 未知状态(服务器重启后的初始状态,等待第一次心跳同步) + */ + UNKNOWN, + /** * 离线 */ diff --git a/src/main/java/com/tuoheng/status/machine/status/CoverState.java b/src/main/java/com/tuoheng/status/machine/status/CoverState.java index a3c463a..5f58d3b 100644 --- a/src/main/java/com/tuoheng/status/machine/status/CoverState.java +++ b/src/main/java/com/tuoheng/status/machine/status/CoverState.java @@ -4,6 +4,11 @@ package com.tuoheng.status.machine.status; * 舱门状态枚举 */ public enum CoverState { + /** + * 未知状态(服务器重启后的初始状态,等待第一次心跳同步) + */ + UNKNOWN, + /** * 舱门已关闭 */ @@ -24,11 +29,6 @@ public enum CoverState { */ CLOSING, - /** - * 舱门半开 - */ - HALF_OPEN, - /** * 舱门状态异常 */