添加状态初始化能力

This commit is contained in:
孙小云 2025-12-15 19:57:00 +08:00
parent 89e49dd17d
commit 51fe7cf816
11 changed files with 327 additions and 10 deletions

View File

@ -70,8 +70,9 @@ public class AirportMachineConfig {
private void configureStates(StateMachineBuilder.Builder<AirportState, AirportEvent> 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<AirportState, AirportEvent> 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)

View File

@ -70,7 +70,7 @@ public class CoverMachineConfig {
private void configureCoverStates(StateMachineBuilder.Builder<CoverState, CoverEvent> 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<CoverState, CoverEvent> 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)

View File

@ -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");

View File

@ -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;
}
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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<String, AirportState> airportStateMap = new ConcurrentHashMap<>();
private final Map<String, CoverState> 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<AirportState> loadAirportState(String airportSn) {
return Optional.ofNullable(airportStateMap.get(airportSn));
}
public Optional<CoverState> loadCoverState(String airportSn) {
return Optional.ofNullable(coverStateMap.get(airportSn));
}
public Set<String> allAirportIds() {
// 合并两张表的 key防止有只存机场或只存舱门的情况
Set<String> ids = ConcurrentHashMap.newKeySet();
ids.addAll(airportStateMap.keySet());
ids.addAll(coverStateMap.keySet());
return ids;
}
}

View File

@ -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<AirportState, AirportEvent> stateMachineFactory;
@Autowired
private RedisStateStore redisStateStore;
/**
* 存储所有机巢的状态机实例
* Key: 机巢ID (airportSn)
@ -37,8 +42,11 @@ public class AirportMachineService {
public StateMachine<AirportState, AirportEvent> getOrCreateStateMachine(String airportSn) {
return stateMachineMap.computeIfAbsent(airportSn, id -> {
StateMachine<AirportState, AirportEvent> 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 {

View File

@ -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<CoverState, CoverEvent> coverStateMachineFactory;
@Autowired
private RedisStateStore redisStateStore;
private final Map<String, StateMachine<CoverState, CoverEvent>> stateMachineMap = new ConcurrentHashMap<>();
public StateMachine<CoverState, CoverEvent> getOrCreateStateMachine(String airportSn) {
return stateMachineMap.computeIfAbsent(airportSn, id -> {
StateMachine<CoverState, CoverEvent> 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 {

View File

@ -4,6 +4,11 @@ package com.tuoheng.status.machine.status;
* 机巢状态枚举简化版 - 舱门状态已分离
*/
public enum AirportState {
/**
* 未知状态服务器重启后的初始状态等待第一次心跳同步
*/
UNKNOWN,
/**
* 离线
*/

View File

@ -4,6 +4,11 @@ package com.tuoheng.status.machine.status;
* 舱门状态枚举
*/
public enum CoverState {
/**
* 未知状态服务器重启后的初始状态等待第一次心跳同步
*/
UNKNOWN,
/**
* 舱门已关闭
*/
@ -24,11 +29,6 @@ public enum CoverState {
*/
CLOSING,
/**
* 舱门半开
*/
HALF_OPEN,
/**
* 舱门状态异常
*/