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

Система движений и регистров

Назначение

Система движений — ядро складского учёта WMS. Каждое действие, влияющее на остатки, положение контейнеров, серийные номера или блокировки, порождает движения. Движения применяются к регистрам (числовым накопителям) и объектам (перемещаемым сущностям).

Вызывается из конвейера обработки документов на шаге 8 (BaseDocManager.saveInnerMovementManager.processDocument).

Базовые интерфейсы

Movement<C, O>                          — корневой интерфейс всех движений
├── RegistryMovement<C, O extends Registry> — регистровые (изменение числовых значений)
└── ObjectMovement<C, O extends MovableObject> — объектные (перемещение объектов)

Registry<C, O> — числовой регистр (координата → значения)
├── RegistryWithSplitter — с поддержкой параллельных записей
├── RegistryWithHistory — с историческими снимками
└── RegistryWithGroupValidation — с групповой валидацией

MovableObject<C, O> — перемещаемый объект (координата → объект)

Типы движений

ТипКатегорияРегистр/ОбъектНазначение
StorageRegistryMovementRegistryStorageRegistryОстатки товара (quantity + reserved) на складских местах. Основной регистр. Поддерживает splitter и историю
WorkflowRegistryMovementRegistryWorkflowRegistryТовары/контейнеры в рабочих процессах. Отслеживание "в работе" по задачам. Поддерживает splitter
ControlRegistryMovementRegistryControlRegistryБлокировка складских операций. Хранит флаг blocked. Максимально широкая координата
ShipmentControlRegistryMovementRegistryShipmentControlRegistryКонтроль этапов отгрузки по заказам. Поддерживает splitter
ContainerMovementObjectBaseContainerПеремещение контейнеров между местами
SerialNumberMovementObjectSerialNumberПеремещение серийных номеров (маркировка, ЧЗ)

Координаты

Координата — value-object, определяющий уникальную позицию в регистре. Имеет корректные equals/hashCode.

StorageRegistryCoordinate

ПолеТипОписание
skuPackagingIdUUIDУпаковка товара
skuBatchIdUUIDПартия
conditionEnumКондиция (годный, брак, карантин)
warehouseSpaceWarehouseSpaceСкладское место
sourceDocumentIdUUIDДокумент-источник

WorkflowRegistryCoordinate

ПолеТипОписание
containerIdUUIDКонтейнер
skuPackagingIdUUIDУпаковка
skuBatchIdUUIDПартия
conditionEnumКондиция
warehouseSpaceIdUUIDМесто
workflowTaskIdUUIDЗадача workflow

ControlRegistryCoordinate

Максимально широкая координата — блокировка на любом уровне иерархии:

ПолеОписание
skuIdКонкретный товар
merchantIdМерчант
skuBatchIdПартия
skuCategoryIdКатегория товара
containerIdКонтейнер
warehouseSpaceIdСкладское место
warehouseIdСклад целиком
warehouseZoneIdЗона склада
warehouseTopologyТопология
warehouseOperationТип операции

ShipmentControlRegistryCoordinate

ПолеОписание
orderDocIdЗаказ
skuIdТовар
skuBatchIdПартия
containerIdКонтейнер
stageЭтап отгрузки (ShipmentStage)

ContainerCoordinate

ПолеОписание
reservedПризнак резервирования
warehouseSpaceМесто размещения
reservedForDocumentIdДля какого документа

SerialNumberCoordinate

ПолеОписание
conditionКондиция
warehouseSpaceМесто размещения

Сервисы

СервисРоль
MovementManagerЦентральный сервис. processDocument — извлекает движения из документов, обрабатывает по типам
StockServiceЗапросы складских остатков. Нативный SQL к wms_storage_registry с гибкой группировкой
MovementScheduleManagerПланировщик: исторические снимки, удаление незавершённых перемещений
*MovementEntityServiceПо одному на каждый тип движения: поиск, удаление, подготовка данных
*RegistryEntityService / *MovableObjectEntityServiceПо одному на каждый тип регистра/объекта

Алгоритм обработки (MovementManager.processDocument)

processDocument(documents, items)

├─ 1. Сбор движений из GenerateMovements и HasItemsWithMovements
│ (для удалённых документов движения не генерируются)

├─ 2. Проверка блокировок (checkMovementIsNotBlocked)
│ StorageRegistryMovement и ContainerMovement проверяются в ControlRegistry
│ Исключение: полная инвентаризация игнорирует блокировки

├─ 3. Последовательная обработка по типам (строгий порядок):
│ ControlRegistryMovement → первыми (предотвращение недопустимых операций)
│ ContainerMovement → перемещение контейнеров
│ StorageRegistryMovement → изменение остатков
│ WorkflowRegistryMovement → учёт в процессах
│ SerialNumberMovement → перемещение серийных номеров
│ ShipmentControlRegistryMovement → контроль отгрузки

├─ 4. Проверка резервов:
│ • quantity >= reserved (по складу, типу зоны, товару, кондиции)
│ • quantity >= reserved (по каждой корневой ячейке)
│ • Ограничения на смешивание товаров/партий в ячейках

└─ 5. Публикация ContainerContentChangedEvent (если затронуты контейнеры)

Обработка регистровых движений (processRegistryMovements)

  1. Загрузка старых движений документа из БД
  2. Инвертирование старых движений (from↔to) для отмены предыдущей проводки
  3. Сохранение новых движений в БД
  4. Загрузка записей регистра по координатам (с SELECT ... FOR UPDATE для splitter-регистров)
  5. Создание новых записей регистра, если не существуют (нулевые значения)
  6. Для регистров с историей — создание снимков на каждый месяц в периоде
  7. Применение apply() каждого движения ко всем подходящим временным срезам
  8. Удаление пустых регистров (все значения = 0)
  9. Групповая валидация: записи группируются по validationGroupHash, суммируются, проверяются на неотрицательность
  10. Удаление старых движений из БД

Обработка объектных движений (processObjectMovements)

Аналогичная схема "отмена старых + применение новых":

  • Проверка на дублирование: один объект → одни координаты — несколько раз = ошибка
  • Оптимизация: идентичные движения (тот же объект, те же координаты) не перепроводятся
  • После применения — validateAll для проверки корректности

Механизм Splitter

Проблема: конкурентный доступ к одной координате регистра → конфликт блокировок.

Решение: при LockTimeoutException на SELECT ... FOR UPDATE:

  1. Инкрементируется значение splitter
  2. Попытка заблокировать запись с новым splitter
  3. Если записи нет — создаётся новая
  4. Несколько потоков модифицируют один регистр через разные строки
  5. При валидации — строки с одной координатой, но разными splitter, суммируются

Исторические снимки

Для StorageRegistry ведутся снимки на начало каждого месяца:

  • Запись с dateTime = null — актуальное значение
  • Запись с конкретным dateTime — снимок на начало месяца
  • MovementScheduleManager ежедневно создаёт снимки на следующий месяц
  • При движении с датой в прошлом — создаются недостающие промежуточные снимки

Генерация движений в документах

Документы и позиции реализуют интерфейс GenerateMovements:

  • getMovements() — список движений, определяемых состоянием документа
  • getMovementTypes() — набор поддерживаемых классов движений

Для документов с позициями — HasItemsWithMovements.getMovementsFromItems() агрегирует движения из всех неудалённых позиций.

Особенности

  • Идемпотентность: При повторном сохранении старые движения полностью откатываются, новые проводятся заново
  • Двойная проводка: Регистровое движение с обеими координатами вычитает из "откуда" и прибавляет к "куда"
  • Retry: ConstraintViolationExceptionDataIntegrityViolationException → перехват @RetryIfOptimisticLock
  • Каскадное обновление контейнеров: ContainerContentChangedEvent → обновление агрегатов SkuItems