Перейти к основному содержимому

Полное руководство по созданию и использованию State Machine в WMS

Документ описывает полный цикл разработки модулей на базе Spring StateMachine и максимально сохраняет формулировки внутренних рекомендаций команды. Здесь собраны все ключевые детали: определение State / Event, работу с AvailableAction / RequiredAction, использование EventParam / StateParam, конфигурацию переходов, а также персистенцию и восстановление контекста.


1. Набор State, Event, RequiredAction и AvailableAction

Описываем набор состояний (State) и событий (Event) для state machine, а так же доступные (AvailableAction) и необходимые (RequiredAction) действия, которые будем отдавать фронту, чтобы он запросил данные у пользователя.

1.1 Состояния (State)

Состояний наследуем от BaseState и делаем ссылки на CommonState, если там есть аналоги (например для ожидания ввода партий, ЧЗ, кол-ва и т.д.). Состояния примерно соответствуют узлам в схеме бизнес‑логики.

public enum MyProcessState implements BaseState {
INIT(null),
STEP_ONE(CommonState.AWAITING_SKU), // ожидание ввода партий
STEP_TWO(null),
REVIEW(null),
COMPLETE(null);

private final CommonState parent;
MyProcessState(CommonState parent) { this.parent = parent; }
@Override public CommonState getValue() { return parent; }
}

1.2 События (Event)

События будут вызываться на HTTP‑запросы из контроллера и, как правило, отражают ввод каких‑то данных от пользователя.

public enum MyProcessEvent {
ON_JOIN,
ON_ENTER_DATA,
ON_CONFIRM,
ON_CANCEL
}

1.3 RequiredAction и AvailableAction

RequiredAction так же наследуем от BaseRequiredAction со ссылками на CommonRequiredAction. Для получения большинства данных от пользователя их нужно запросить, отправив фронту значение RequiredAction.

public enum MyRequiredAction implements BaseRequiredAction {
ENTER_DATA(CommonRequiredAction.ENTER_SKU),
CONFIRM(null);

private final CommonRequiredAction delegate;
MyRequiredAction(CommonRequiredAction delegate) { this.delegate = delegate; }
@Override public CommonRequiredAction getValue() { return delegate; }
}

AvailableAction — действия, которые фронт может инициировать сразу.

public enum MyAvailableAction implements EnumClass<String> {
START("START_PROCESS"),
CANCEL("CANCEL_PROCESS");

private final String code;
MyAvailableAction(String code) { this.code = code; }
@Override public String getValue() { return code; }
}

Таким образом в большинстве случаев создаем цепочки из состояния (State) ожидания чего‑то, RequiredAction к нему для фронта и Event'а для обработки полученных данных.


2. EventParam и StateParam

Иногда могут понадобиться ещё параметры событий (EventParam) для передачи данных из контроллера в state machine и параметры состояний (StateParam) для сохранения данных в машине после обработки события. Но во многих случаях хватает базовых значений из BaseEventParam и BaseStateParam. По возможности используем их.

2.1 Базовые параметры

  • BaseEventParam: DATA, DOCUMENT, CONTAINER
  • BaseStateParam: ROOT_DOC_ID, WORKSPACE_ID, EMPLOYEE … Временные параметры помечаем префиксом TMP_.

2.2 Собственные enum’ы

В своих enum'ах параметров состояний имплементируем StateParam. В качестве параметра у значений указываем boolean‑флаг, определяющий, будет ли параметр сохранен между сессиями.

public enum MyProcessStateParam implements StateParam {
TMP_CUSTOM_VALUE(true), // очищается при write()
PERSISTED_COUNTER(false); // хранится между запросами

private final boolean temp;
MyProcessStateParam(boolean temp) { this.temp = temp; }
@Override public Boolean getValue() { return temp; }
}

public enum MyProcessEventParam implements EventParam {
CUSTOM_DATA;
}

Примеры можно посмотреть в пакете ai.sigmation.wms.model.enums.api.operation.


3. Конфигурация State Machine

Описываем конфигурацию машины состояний (задаем иерархию состояний и возможные переходы между ними).

3.1 Иерархия состояний

Состояния могут быть выстроены в иерархию с помощью указания .parent в конфигурации. Это позволяет делать вложенные машины состояний, что позволяет поддерживать несколько активных состояний одновременно.

@Override
public void configure(StateMachineStateConfigurer<MyProcessState, MyProcessEvent> states) throws Exception {
states.withStates()
.initial(MyProcessState.INIT)
.state(MyProcessState.COMPLETE)
.and().withStates()
.parent(MyProcessState.INIT)
.initial(MyProcessState.STEP_ONE)
.state(MyProcessState.STEP_TWO)
.state(MyProcessState.REVIEW);
}

3.2 Переходы

В конфигурации переходов можно использовать .guard (для доп. проверок на допустимость перехода) и .action (для описания логики, которая должна быть выполнена во время перехода).

@Override
public void configure(StateMachineTransitionConfigurer<MyProcessState, MyProcessEvent> transitions) throws Exception {
transitions.withExternal()
.source(MyProcessState.INIT)
.target(MyProcessState.STEP_ONE)
.event(MyProcessEvent.ON_JOIN)
.action(myActions.joinAction())
.guard(myGuards.canJoin());

transitions.withExternal()
.source(MyProcessState.STEP_ONE)
.target(MyProcessState.STEP_TWO)
.event(MyProcessEvent.ON_ENTER_DATA)
.action(myActions.enterDataAction());
}

Существует набор стандартных guard и action в пакете ai.sigmation.wms.service.api.employeeapi.state.common. По возможности их нужно переиспользовать и наследоваться от базовых (Base). Там есть готовая логика для запроса и обработки данных по товару, входу и выходу из АРМа, создания и сохранения позиций документов и т.д. Примеры — классы с суффиксом StateMachineConfig.


4. Персистенция и восстановление контекста

Описываем логику восстановления состояний машины на основе данных из БД. Свой компонент наследуем от BaseStateMachinePersist и реализуем метод restoreContext, в котором нужно сформировать контекст, определяющий состояние машины.

4.1 Ключевые моменты

  • Если в машине есть вложенные подмашины, контекст должен содержать вложенные контексты (во вложенных важны только state и иерархия), чтобы они не запускались заново после восстановления.
  • В корневой контекст передаем extendedState, содержащий, как минимум, ROOT_DOC_ID, WORKSPACE_ID, EMPLOYEE. Остальные данные определяются бизнес‑логикой.
@Component
public class MyStateMachinePersist extends BaseStateMachinePersist<MyProcessState, MyProcessEvent> {
@Override
protected StateMachineContext<MyProcessState, MyProcessEvent> restoreContext(StateKey key) {
var ext = new DefaultExtendedState();
ext.getVariables().put(BaseStateParam.ROOT_DOC_ID, key.getDocumentId());
ext.getVariables().put(BaseStateParam.WORKSPACE_ID, key.getWorkspaceId());
ext.getVariables().put(BaseStateParam.EMPLOYEE, key.getEmployee());
return new DefaultStateMachineContext<>(MyProcessState.INIT, null, null, null, ext);
}
}

Примеры — классы с суффиксом StateMachinePersist в пакете ai.sigmation.wms.service.api.employeeapi.state.


5. Контроллеры и цепочка State → RequiredAction → Event

@RestController
@RequestMapping("/process")
@RequiredArgsConstructor
public class MyProcessController {
private final MyStateMachineFacade facade;

@PostMapping("/{id}/enter-data")
public ResponseEntity<MyResponseDto> enterData(@PathVariable UUID id, @RequestBody EnterDataDto dto) throws Exception {
return facade.executeWithStateMachine(id, sm -> {
// 1) Отправляем Event с данными
var msg = MessageBuilder.withPayload(MyProcessEvent.ON_ENTER_DATA)
.setHeader(BaseEventParam.DATA.name(), dto.getValue())
.build();
sm.sendEvent(msg);
// 2) Возвращаем фронту новую RequiredAction (если она есть)
return ResponseEntity.ok(buildResponse(sm));
});
}
}

6. Практические советы

  1. Single Responsibility — каждый Guard или Action делает одну вещь.
  2. Переиспользуйте готовые базовые классы из common‑пакета.
  3. Явно описывайте параметры: сначала пробуйте BaseEventParam / BaseStateParam, затем — свои enum’ы.
  4. Fail‑fast — валидируйте входные данные в Action, бросайте исключения сразу.

Соблюдая изложенные правила и примеры, вы сможете создавать новые State Machine‑модули без потери контекста, с предсказуемым поведением и минимальным дублированием кода.