Редизайн pcp-tgu-service: хранить только выданное, будущее считать на лету

Раньше вся будущая цепочка планов материализовалась в planned_plan как
PLANNED-строки, а отдельный воркер пересчёта (RebuildPlanScheduleUseCase +
PlanRecalculationJob) держал её в актуальном состоянии и гасил выпавшие планы
в SUPERSEDED/EXPIRED. Это плодило вложенные планы, гонки пересчёта и мусор в БД.

Новая модель: в БД живёт только выданное (история навсегда), будущее — проекция,
вычисляемая по запросу из ЗРВ. Перекрывать (supersede) и протухать (expire)
больше нечего.

Бэкенд:
- Удалены TrackedPlan (entity/source/status + repo) и PlanRecalculationJob
  (entity/repo/service/worker) — материализация и воркер пересчёта больше не нужны.
- Удалён RebuildPlanScheduleUseCase; будущее строит новый PlanProjectionService
  поверх PlanCalculationService.
- PlanQueryService отдаёт проекцию (выданное из БД + будущее на лету).
- Новый флоу реакции на смену параметров станции: StationParamsChangedConsumer +
  HandleStationParamsChangedUseCase (срочный перевыпуск через planning_state).
- Новые консьюмеры обратной связи исполнения: LayinFailedConsumer,
  PlanNotAcceptedConsumer (+ DTO PlanExecutionFeedbackMessage,
  StationParamsChangedMessage).
- PlannedPlanStatus: убраны мёртвые SUPERSEDED/EXPIRED; статусы выровнены под
  жизненный цикл выдачи/закладки.

Миграции (ddl-auto: validate):
- V7  drop tracked_plan
- V8  drop plan_recalculation_job
- V9  planning_state: поля срочного перевыпуска
- V10 снять unique-ограничение бизнес-ключа planned_plan
- V11 удалить легаси PLANNED-строки (материализованное будущее)
- V12 удалить легаси SUPERSEDED/EXPIRED и столбец superseded_by_plan_id

Тесты: удалены JobRerunFlowTest и RebuildPlanScheduleUseCaseTest; добавлены
PlanProjectionServiceTest, PlanQueryServiceTest, IssueDuePlanWorkerOnDemandTest,
HandleStationParamsChangedUseCaseTest, VisibilityPayloadParserTest. Скорректированы
PlanIssueAndDecisionFlowTest, PlanCalculationServiceTest, VisibilityFlowTest и др.

V11/V12 необратимы — перед миграцией на проде снять дамп planned_plan.
This commit is contained in:
Дмитрий Соловьев
2026-06-02 14:32:51 +03:00
parent d630a891f6
commit c76a823dbc
52 changed files with 1587 additions and 1377 deletions
@@ -14,19 +14,23 @@ spring:
planning:
notification-before-start-minutes: 263
target-plan-duration-minutes: 180
target-plan-duration-delta-minutes: 60
target-plan-duration-minutes: 1440
target-plan-duration-delta-minutes: 240
external-update-enabled: true
external-points-lookahead-days: 3
external-points-lookahead-days: 10
in-memory-history-days: 14
external-update-fixed-delay-ms: 600000
notification-check-fixed-delay-ms: 30000
min-reissue-lead-minutes: 5
reissue-prep-minutes: 30
worker-lock-ttl-seconds: 300
topics:
visibility-windows-changed: pcp.tgu.visibility-windows-changed.v1
satellite-plan-decision: pcp.tgu.satellite-plan-decision.v1
layin-failed: pcp.tgu.layin-failed.v1
plan-not-accepted: pcp.tgu.plan-not-accepted.v1
station-params-changed: pcp.tgu.station-params-changed.v1
tgu:
test-controller:
@@ -0,0 +1,5 @@
-- В on-demand-модели планы не апсертятся по бизнес-ключу: при перевыдаче того же окна
-- (после REJECTED/негатива) создаётся новая строка с новым plan_id, а старая остаётся в истории.
-- Поэтому уникальность по (spacecraft_id, start_time, end_time, kpp_id) больше не нужна.
ALTER TABLE planned_plan
DROP CONSTRAINT IF EXISTS uq_planned_plan_business_key;
@@ -0,0 +1,11 @@
-- Чистка легаси: до редизайна старый код материализовал всю будущую цепочку в planned_plan
-- со статусом PLANNED. Новая модель «храним только выданное» проекцию в БД не пишет — будущее
-- считается на лету. Persisted-строк в статусе PLANNED как класса больше не существует.
--
-- Эти строки и порождали видимые «дубликаты» в UI: persisted-PLANNED (микросекунды) показывался
-- рядом со своим проекционным двойником (наносекунды), не схлопываясь по бизнес-ключу.
--
-- Удаляем только PLANNED. Выданная история (ISSUING/WAITING_DECISION/ACCEPTED/AWAITING_LAYIN/
-- LAID_IN/REJECTED/LAYIN_FAILED/NOT_ACCEPTED/SUPERSEDED/EXPIRED и пр.) не затрагивается.
DELETE FROM planned_plan
WHERE status = 'PLANNED';
@@ -0,0 +1,19 @@
-- Чистка легаси статусов SUPERSEDED/EXPIRED.
--
-- В старой модели вся будущая цепочка материализовалась в planned_plan как PLANNED-строки.
-- SUPERSEDED гасил будущие планы, выпавшие из пересчитанной цепочки (это делал ныне удалённый
-- RebuildPlanScheduleUseCase); EXPIRED помечал неизданный план, чьё окно прошло.
--
-- Новая модель «храним только выданное, будущее считаем на лету» проекцию в БД не пишет:
-- перекрывать (supersede) нечего, а неизданный просроченный план строкой никогда не был.
-- Оба статуса в коде больше не присваиваются (убраны из PlannedPlanStatus).
--
-- Эти строки — не выданная история, а легаси-бухгалтерия будущего, поэтому удаляем безвозвратно.
-- Выданная история (ISSUING/WAITING_DECISION/ACCEPTED/AWAITING_LAYIN/LAID_IN/REJECTED/
-- LAYIN_FAILED/NOT_ACCEPTED/START_AMBIGUOUS) не затрагивается.
DELETE FROM planned_plan
WHERE status IN ('SUPERSEDED', 'EXPIRED');
-- Столбец под supersede-флоу больше не используется (поле supersededByPlanId удалено из сущности).
ALTER TABLE planned_plan
DROP COLUMN IF EXISTS superseded_by_plan_id;
@@ -0,0 +1,2 @@
-- Legacy-таблица из V1/V2: заменена на planned_plan (V4), в коде не используется.
DROP TABLE IF EXISTS tracked_plan;
@@ -0,0 +1,2 @@
-- Расписание считается on-demand из снапшота ЗРВ; отдельная очередь пересчёта больше не нужна.
DROP TABLE IF EXISTS plan_recalculation_job;
@@ -0,0 +1,4 @@
-- Время, начиная с которого нужно срочно выдать следующий план после негативного события
-- (REJECTED / «не заложен» / «не принят»). NULL — срочной перевыдачи не требуется.
ALTER TABLE spacecraft_planning_state
ADD COLUMN IF NOT EXISTS urgent_reissue_from TIMESTAMP NULL;