pcp-tgu-service: не дублировать выданный план проекцией
Проекция будущей цепочки якорилась только на принятый базовый план (currentBasePlanId) и игнорировала выданный, но ещё не принятый план в полёте (ISSUING/WAITING_DECISION). Из-за этого она заново выводила уже выданный слот, и при дрейфе снапшота ЗРВ (сместился КПП или границы окна) точечный дедуп по businessKey в PlanQueryService не срабатывал — на вкладке «Планы» появлялся призрак-дубль PLANNED поверх WAITING_DECISION. Теперь якорь проекции — самая дальняя зафиксированная точка цепочки: если есть план в полёте, цепочку строим после него. На путь выдачи не влияет: там activePlanId к моменту вызова проекции уже снят.
This commit is contained in:
+33
-4
@@ -4,6 +4,8 @@ import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import space.nstart.pcp_tgu_service.domain.CalculatedPlan
|
||||
import space.nstart.pcp_tgu_service.entity.PlannedPlanEntity
|
||||
import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
|
||||
import space.nstart.pcp_tgu_service.entity.SpacecraftPlanningStateEntity
|
||||
import space.nstart.pcp_tgu_service.integration.api.VisibilityPayloadParser
|
||||
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
|
||||
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
|
||||
@@ -31,16 +33,38 @@ class PlanProjectionService(
|
||||
val points = visibilityPayloadParser.parse(spacecraftId, snapshot.payloadJson)
|
||||
|
||||
val state = spacecraftPlanningStateJpaRepository.findById(spacecraftId).orElse(null)
|
||||
val basePlan = state?.currentBasePlanId
|
||||
?.let { planId -> plannedPlanRepository.findById(planId).orElse(null) }
|
||||
val anchorPlan = resolveAnchorPlan(state)
|
||||
|
||||
return if (basePlan == null) {
|
||||
return if (anchorPlan == null) {
|
||||
planCalculationService.calculatePlans(points)
|
||||
} else {
|
||||
planCalculationService.calculatePlansAfter(points, basePlan.toCalculatedPlan())
|
||||
planCalculationService.calculatePlansAfter(points, anchorPlan.toCalculatedPlan())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Якорь, после которого строится будущая цепочка: самая дальняя уже зафиксированная точка.
|
||||
*
|
||||
* Если есть выданный, но ещё не принятый план в полёте (ISSUING/WAITING_DECISION) — цепочку
|
||||
* продолжаем ПОСЛЕ него, а не после принятого базового плана. Иначе проекция заново вывела бы
|
||||
* уже выданный слот и при дрейфе снапшота ЗРВ (сместился КПП или границы окна) нарисовала бы
|
||||
* его призрак-дубль PLANNED рядом с реальным WAITING_DECISION: точечный дедуп по
|
||||
* businessKey в [PlanQueryService] такой рассинхрон не ловит.
|
||||
*
|
||||
* `activePlanId` заполнен только вместе с активной выдачей; в пути выдачи он к моменту
|
||||
* вызова проекции уже снят, поэтому якорение на план в полёте не влияет на выбор головы.
|
||||
*/
|
||||
private fun resolveAnchorPlan(state: SpacecraftPlanningStateEntity?): PlannedPlanEntity? {
|
||||
val inFlight = state?.activePlanId
|
||||
?.let { planId -> plannedPlanRepository.findById(planId).orElse(null) }
|
||||
?.takeIf { plan -> plan.status in IN_FLIGHT_STATUSES }
|
||||
if (inFlight != null) {
|
||||
return inFlight
|
||||
}
|
||||
return state?.currentBasePlanId
|
||||
?.let { planId -> plannedPlanRepository.findById(planId).orElse(null) }
|
||||
}
|
||||
|
||||
private fun PlannedPlanEntity.toCalculatedPlan(): CalculatedPlan =
|
||||
CalculatedPlan(
|
||||
spacecraftId = spacecraftId,
|
||||
@@ -48,4 +72,9 @@ class PlanProjectionService(
|
||||
endTime = endTime,
|
||||
kppId = kppId
|
||||
)
|
||||
|
||||
private companion object {
|
||||
/** Статусы выданного, но ещё не принятого плана — он уже фиксирует слот цепочки. */
|
||||
val IN_FLIGHT_STATUSES = setOf(PlannedPlanStatus.ISSUING, PlannedPlanStatus.WAITING_DECISION)
|
||||
}
|
||||
}
|
||||
|
||||
+77
@@ -76,10 +76,66 @@ class PlanProjectionServiceTest {
|
||||
verify(calculation, never()).calculatePlans(anyNonNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `anchors the tail after the in-flight issued plan, not the accepted base`() {
|
||||
val base = persistedPlan(time(0), time(3), "KPP-base", PlannedPlanStatus.AWAITING_LAYIN)
|
||||
val inFlight = persistedPlan(time(2), time(5), "KPP-inflight", PlannedPlanStatus.WAITING_DECISION)
|
||||
val calculation = mock(PlanCalculationService::class.java)
|
||||
val expected = listOf(plan(time(4), time(7)))
|
||||
var anchor: CalculatedPlan? = null
|
||||
`when`(calculation.calculatePlansAfter(anyNonNull(), anyNonNull())).thenAnswer { invocation ->
|
||||
anchor = invocation.getArgument(1)
|
||||
expected
|
||||
}
|
||||
val service = service(
|
||||
state = SpacecraftPlanningStateEntity(
|
||||
spacecraftId = spacecraftId,
|
||||
currentBasePlanId = base.planId,
|
||||
activePlanId = inFlight.planId
|
||||
),
|
||||
plansById = mapOf(base.planId to base, inFlight.planId to inFlight),
|
||||
calculation = calculation
|
||||
)
|
||||
|
||||
val result = service.project(spacecraftId)
|
||||
|
||||
assertEquals(expected, result)
|
||||
assertEquals(inFlight.startTime, anchor?.startTime)
|
||||
assertEquals(inFlight.endTime, anchor?.endTime)
|
||||
assertEquals(inFlight.kppId, anchor?.kppId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `falls back to the base plan when the active plan is no longer in flight`() {
|
||||
val base = persistedPlan(time(0), time(3), "KPP-base", PlannedPlanStatus.AWAITING_LAYIN)
|
||||
val accepted = persistedPlan(time(2), time(5), "KPP-accepted", PlannedPlanStatus.AWAITING_LAYIN)
|
||||
val calculation = mock(PlanCalculationService::class.java)
|
||||
var anchor: CalculatedPlan? = null
|
||||
`when`(calculation.calculatePlansAfter(anyNonNull(), anyNonNull())).thenAnswer { invocation ->
|
||||
anchor = invocation.getArgument(1)
|
||||
emptyList<CalculatedPlan>()
|
||||
}
|
||||
val service = service(
|
||||
state = SpacecraftPlanningStateEntity(
|
||||
spacecraftId = spacecraftId,
|
||||
currentBasePlanId = base.planId,
|
||||
activePlanId = accepted.planId
|
||||
),
|
||||
plansById = mapOf(base.planId to base, accepted.planId to accepted),
|
||||
calculation = calculation
|
||||
)
|
||||
|
||||
service.project(spacecraftId)
|
||||
|
||||
assertEquals(base.startTime, anchor?.startTime)
|
||||
assertEquals(base.kppId, anchor?.kppId)
|
||||
}
|
||||
|
||||
private fun service(
|
||||
snapshot: SpacecraftVisibilitySnapshotEntity? = snapshot(),
|
||||
state: SpacecraftPlanningStateEntity? = null,
|
||||
basePlan: PlannedPlanEntity? = null,
|
||||
plansById: Map<UUID, PlannedPlanEntity>? = null,
|
||||
calculation: PlanCalculationService
|
||||
): PlanProjectionService {
|
||||
val snapshotRepository = mock(SpacecraftVisibilitySnapshotRepository::class.java)
|
||||
@@ -87,12 +143,33 @@ class PlanProjectionServiceTest {
|
||||
val stateRepository = mock(SpacecraftPlanningStateJpaRepository::class.java)
|
||||
`when`(stateRepository.findById(spacecraftId)).thenReturn(Optional.ofNullable(state))
|
||||
val plannedRepository = mock(PlannedPlanRepository::class.java)
|
||||
if (plansById != null) {
|
||||
`when`(plannedRepository.findById(anyNonNull())).thenAnswer { invocation ->
|
||||
Optional.ofNullable(plansById[invocation.getArgument(0)])
|
||||
}
|
||||
} else {
|
||||
`when`(plannedRepository.findById(anyNonNull())).thenReturn(Optional.ofNullable(basePlan))
|
||||
}
|
||||
val parser = mock(VisibilityPayloadParser::class.java)
|
||||
`when`(parser.parse(anyNonNull(), anyNonNull())).thenReturn(SpacecraftPoints(spacecraftId, emptyList()))
|
||||
return PlanProjectionService(snapshotRepository, stateRepository, plannedRepository, parser, calculation)
|
||||
}
|
||||
|
||||
private fun persistedPlan(
|
||||
start: LocalDateTime,
|
||||
end: LocalDateTime,
|
||||
kppId: String,
|
||||
status: PlannedPlanStatus
|
||||
): PlannedPlanEntity =
|
||||
PlannedPlanEntity(
|
||||
planId = UUID.randomUUID(),
|
||||
spacecraftId = spacecraftId,
|
||||
startTime = start,
|
||||
endTime = end,
|
||||
kppId = kppId,
|
||||
status = status
|
||||
)
|
||||
|
||||
private fun snapshot(): SpacecraftVisibilitySnapshotEntity =
|
||||
SpacecraftVisibilitySnapshotEntity(
|
||||
spacecraftId = spacecraftId,
|
||||
|
||||
Reference in New Issue
Block a user