pcp-tgu: якорь цепочки на LAID_IN, параллельные выдачи, SUPERSEDED

Базой цепочки план становится на ЗАКЛАДКЕ (LAID_IN), а не на приёмке.
После принятого плана можно отправить коррекцию — она тоже примется;
несколько выдач «в полёте» по одному КА теперь норма, а не конфликт.

- ACCEPT больше не двигает currentBasePlanId/chainVersion и не запирает
  слот — только переводит план в AWAITING_LAYIN.
- Новый позитивный топик layin-confirmed + LayinConfirmedConsumer →
  handleLayinConfirmed: AWAITING_LAYIN → LAID_IN, план становится базой
  (chainVersion++), конкуренты на пересекающемся окне гасятся в новый
  терминальный статус SUPERSEDED. Гард ±10 мин (LAYIN_MIN_SEPARATION_MINUTES):
  нельзя заложить план со стартом в пределах 10 мин от уже заложенного (409).
- «В полёте» считается из статусов планов (ISSUING/WAITING_DECISION), а не
  из единственного active-слота; снят гард «идёт выдача другого плана».
  IssueDuePlanWorker гейтится hasBlockingInFlight, базу нового плана берёт
  как latestCommittedPlanId; grace больше не двигает базу.
- PlanProjectionService якорит по самому дальнему committed-окну
  {ISSUING, WAITING_DECISION, AWAITING_LAYIN, LAID_IN}.
- REJECT ставит паузу только если по КА не осталось живых кандидатов.
- CreateDraftUseCase: replace-черновик наследует ПРЕДШЕСТВЕННИКА исходного
  плана (base.basePlanId), а не сам план — коррекция занимает тот же слот;
  fork-forward по-прежнему ссылается на базу. Идемпотентность черновиков —
  по слоту (КА + окно), а не по basePlanId.
- Тест-контроллер: POST /test/tgu/plans/{id}/confirm-layin (через BFF
  pcp-ui-service) — имитация события «план заложен».
- Док PCP_TGU_PLAN_STATUS_LIFECYCLE обновлён (LAID_IN-переход, SUPERSEDED).
This commit is contained in:
Дмитрий Соловьев
2026-06-04 16:13:02 +03:00
parent 5235142fa6
commit 87905311b1
21 changed files with 558 additions and 240 deletions
@@ -47,6 +47,13 @@ class TguPlanningRestController(
@RequestParam(required = false) reason: String?
) = tguPlanningService.sendDecision(planId, decision, reason)
/** ТЕСТ: подтвердить закладку плана (AWAITING_LAYIN → LAID_IN). */
@PostMapping("/plans/{planId}/confirm-layin")
fun confirmLayin(
@PathVariable planId: UUID,
@RequestParam(required = false) reason: String?
) = tguPlanningService.confirmLayin(planId, reason)
@GetMapping("/spacecraft/{noradId}/flight-line")
fun spacecraftFlightLine(
@PathVariable noradId: Long,
@@ -87,6 +87,26 @@ class TguPlanningService(webClientBuilderProvider: ObjectProvider<WebClient.Buil
throw error
}
fun confirmLayin(planId: UUID, reason: String?) {
runCatching {
webClientBuilder.build()
.post()
.uri(buildUri("/test/tgu/plans/{planId}/confirm-layin") { builder ->
reason?.takeIf { it.isNotBlank() }?.let { builder.queryParam("reason", it) }
builder.buildAndExpand(planId)
})
.retrieve()
.onStatus({ status -> status.isError }, ::mapReadError)
.bodyToMono(Void::class.java)
.block()
}.getOrElse { error ->
if (error is WebClientRequestException) {
throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, TEST_CONTROLLER_UNAVAILABLE_MESSAGE, error)
}
throw error
}
}
private fun buildUri(
path: String,
customizer: (UriComponentsBuilder) -> Any = { builder -> builder.build() }