From 69dfd8025d9478e0c5b8c8d3b10355e8b0883873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=A1=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D1=8C=D0=B5=D0=B2?= Date: Thu, 4 Jun 2026 21:15:13 +0300 Subject: [PATCH] =?UTF-8?q?pcp-tgu:=20=D1=81=D0=BB=D0=BE=D1=82=D1=8B=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=84=D0=B0=D0=BA=D1=82=D1=83=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B8,=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=80=D0=B5=D1=82=20=D1=80=D0=B0=D0=BD=D0=BD=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B8,=20=D1=88?= =?UTF-8?q?=D0=B8=D1=80=D0=B5=20=D1=80=D0=B5=D0=B9=D0=BB=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - слоты (acceptedPlanSlotsUpdate) занимаются на LAID_IN, а не на ACCEPT — до закладки план ещё может быть вытеснен коррекцией - «Подтвердить закладку» недоступно, пока начало плана в будущем - левая панель редактора 15.5rem → 17rem (цифры статистики у границы) --- .../src/features/tgu-editor/TguEditorTab.tsx | 2 +- .../features/tgu-planning/TguPlanDetails.tsx | 20 ++++++++++++++----- .../service/HandlePlanDecisionUseCase.kt | 6 +++++- .../service/PlanIssueAndDecisionFlowTest.kt | 10 +++++++--- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx index ac4e06b..e51ba7f 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx @@ -1032,7 +1032,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, {applyError &&
{applyError}
} -
+
plan.startMs + DECISION_GRACE_MINUTES * 60_000; + // Закладку нельзя подтверждать, пока начало плана ещё в будущем: план закладывается на КА + // по факту наступления своего старта, а не заранее. + const layinStartInFuture = plan.startMs > Date.now(); // «Создать черновик на основе плана» (replace, окно 1:1, все включения) доступно для // персистентных правимых баз. DRAFT уже имеет свои действия; PLANNED — виртуальная проекция, // не база; LAID_IN — replace 1:1 запрещён (слот зафиксирован, только fork-forward). @@ -232,17 +235,24 @@ export function TguPlanDetails({ - {layinConflict && ( + {layinStartInFuture && ( +
+ Начало плана ещё в будущем — подтвердить закладку можно только после наступления его старта. +
+ )} + {!layinStartInFuture && layinConflict && (
По КА уже есть заложенный план со стартом в пределах ±10 минут — подтвердить нельзя.
diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt index 230b7a8..a7cf27f 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt @@ -149,6 +149,10 @@ class HandlePlanDecisionUseCase( plannedPlanRepository.save(plan) spacecraftPlanningStateJpaRepository.save(state) + + // Статусы слотов проставляем только по факту закладки на КА (LAID_IN), а не на ACCEPT: + // до закладки план ещё может быть вытеснен коррекцией, и занимать слоты рано. + acceptedPlanSlotsUpdateJobService.enqueue(plan.planId) } /** Гасит в SUPERSEDED ещё не заложенные планы КА, чьё окно пересекается с заложенным. */ @@ -247,9 +251,9 @@ class HandlePlanDecisionUseCase( // ACCEPT больше НЕ запирает цепочку и не двигает базу/версию: принятый план лишь ждёт // закладки. Базой (currentBasePlanId) и chainVersion управляет только LAID_IN // ([handleLayinConfirmed]) — до закладки можно отправить и принять коррекцию того же окна. + // Статусы слотов тоже проставляются не здесь, а на закладке ([handleLayinConfirmed]). planIssueAttemptRepository.save(attempt) plannedPlanRepository.save(plan) - acceptedPlanSlotsUpdateJobService.enqueue(plan.planId) } private fun rejectPlan( diff --git a/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/service/PlanIssueAndDecisionFlowTest.kt b/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/service/PlanIssueAndDecisionFlowTest.kt index 7e546bb..382f53a 100644 --- a/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/service/PlanIssueAndDecisionFlowTest.kt +++ b/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/service/PlanIssueAndDecisionFlowTest.kt @@ -61,7 +61,7 @@ class PlanIssueAndDecisionFlowTest { } @Test - fun `accepted decision moves plan to AWAITING_LAYIN and enqueues slots without anchoring base`() { + fun `accepted decision moves plan to AWAITING_LAYIN without anchoring base or touching slots`() { val spacecraftId = "25544" val plan = plannedPlan(spacecraftId, time(12), time(13), PlannedPlanStatus.WAITING_DECISION) val attempt = attempt(plan.planId, PlanIssueAttemptStatus.STARTED) @@ -80,7 +80,8 @@ class PlanIssueAndDecisionFlowTest { // ACCEPT больше НЕ делает план базой и не двигает версию — это делает только LAID_IN. assertNull(state.currentBasePlanId) assertEquals(0, state.chainVersion) - verify(slotsUpdateJobService).enqueue(plan.planId) + // Слоты проставляются на закладке (LAID_IN), а не на ACCEPT. + verify(slotsUpdateJobService, never()).enqueue(plan.planId) } @Test @@ -95,12 +96,13 @@ class PlanIssueAndDecisionFlowTest { val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, chainVersion = 4) val processedRepository = mock(ProcessedPlanDecisionEventRepository::class.java) `when`(processedRepository.insertIfAbsent(anyNonNull(), anyNonNull())).thenReturn(1).thenReturn(0) + val slotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java) val useCase = HandlePlanDecisionUseCase( processedPlanDecisionEventRepository = processedRepository, planIssueAttemptRepository = attemptRepository(linkedMapOf()), plannedPlanRepository = plannedRepository(store), spacecraftPlanningStateJpaRepository = stateRepository(state), - acceptedPlanSlotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java), + acceptedPlanSlotsUpdateJobService = slotsUpdateJobService, planningProperties = planningProperties() ) val message = feedbackMessage(spacecraftId, laidIn.planId) @@ -113,6 +115,8 @@ class PlanIssueAndDecisionFlowTest { assertEquals(5, state.chainVersion) assertEquals(PlannedPlanStatus.SUPERSEDED, competitor.status) assertEquals(PlannedPlanStatus.AWAITING_LAYIN, unrelated.status) + // Слоты проставляются по факту закладки — ровно один раз (второй вызов идемпотентен). + verify(slotsUpdateJobService).enqueue(laidIn.planId) } @Test