feat(slots-service): #50 M3 — держатели BUSY-слотов в request-cover
reqCover(all=true) обогащает занятые (BUSY) слоты держателями: какие заявки блокируют слот, с именем и важностью (обзор конфликтов при ручной броне). - SlotHolderDTO (requestId + опц. name/importance), поле holders в SlotDTO (null для AVAILABLE/BOOKED и прочих путей — обратно совместимо). - TestSatelliteImpl: для BUSY-слота собирает держателей по пересечению сдвинутых на cycle интервалов активных броней (та же логика, что band, с mmi), distinct по requestId (множественный держатель). - EarthService.requestHolders: батч-резолв name/importance заявок; деградация через runCatching → emptyMap (request-service недоступен → только id, без падения). - SlotService.enrichHolders: один батч-запрос на обзор, slot.copy(holders=...). - SlotDTO переведён в data class (copy вместо ручного перечисления полей). - Тест: BUSY несёт держателей, BOOKED/AVAILABLE — нет.
This commit is contained in:
+4
-1
@@ -3,7 +3,7 @@ package space.nstart.pcp.pcp_types_lib.dto.requests.slots
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class SlotDTO(
|
||||
data class SlotDTO(
|
||||
val cycle : Long = 0,
|
||||
val satelliteId : Long = 0,
|
||||
val tn : LocalDateTime,
|
||||
@@ -16,4 +16,7 @@ class SlotDTO(
|
||||
val state : SlotStatus = SlotStatus.AVAILABLE,
|
||||
val latitude : Double = 0.0,
|
||||
val longitude : Double = 0.0,
|
||||
// Держатели слота — заполняются только для BUSY-слотов в обзорном request-cover (all=true).
|
||||
// null/пусто для AVAILABLE/BOOKED и для всех прочих путей (обратно совместимо).
|
||||
val holders : List<SlotHolderDTO>? = null,
|
||||
)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package space.nstart.pcp.pcp_types_lib.dto.requests.slots
|
||||
|
||||
/**
|
||||
* Держатель занятого (BUSY) слота — заявка, чья бронь блокирует слот на этом проходе КА.
|
||||
* Заполняется только в обзорном ответе request-cover (all=true); name/importance могут быть null,
|
||||
* если request-service недоступен (деградация — бейдж показывает только идентификатор заявки).
|
||||
*/
|
||||
data class SlotHolderDTO(
|
||||
val requestId: String,
|
||||
val name: String? = null,
|
||||
val importance: Double? = null,
|
||||
)
|
||||
+4
-2
@@ -6,6 +6,7 @@ import org.hibernate.annotations.FetchMode
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotHolderDTO
|
||||
import space.nstart.pcp.pcp_types_lib.utils.wkt.WKTParser
|
||||
import space.nstart.pcp.slots_service.model.Mar
|
||||
import java.time.LocalDateTime
|
||||
@@ -104,7 +105,7 @@ class SlotEntity(
|
||||
longitude
|
||||
)
|
||||
|
||||
fun toDTO(booked : Boolean, band : Boolean) = SlotDTO(
|
||||
fun toDTO(booked : Boolean, band : Boolean, holders : List<SlotHolderDTO>? = null) = SlotDTO(
|
||||
cycle,
|
||||
satelliteId,
|
||||
tn,
|
||||
@@ -116,6 +117,7 @@ class SlotEntity(
|
||||
slotNum,
|
||||
state = if (booked) SlotStatus.BOOKED else if (band) SlotStatus.BUSY else SlotStatus.AVAILABLE,
|
||||
latitude,
|
||||
longitude
|
||||
longitude,
|
||||
holders = holders
|
||||
)
|
||||
}
|
||||
|
||||
+35
-1
@@ -5,6 +5,7 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotHolderDTO
|
||||
import space.nstart.pcp.slots_service.entity.BookedSlotEntity
|
||||
import space.nstart.pcp.slots_service.entity.SlotEntity
|
||||
import space.nstart.pcp.slots_service.model.BookedSlot
|
||||
@@ -38,11 +39,28 @@ class TestSatelliteImpl(
|
||||
states : List<SlotStatus>?
|
||||
): List<PreparedCoverageSlot> {
|
||||
class BookedInfo(val id : Long, val cycle : Long)
|
||||
// Держатель блокирующей брони: сдвинутый на cycle интервал + крен + заявки-держатели.
|
||||
// Нужен, чтобы у BUSY-слота показать, чья бронь его блокирует (M3 — обзор конфликтов).
|
||||
class HolderInterval(
|
||||
val start : LocalDateTime,
|
||||
val end : LocalDateTime,
|
||||
val roll : Double,
|
||||
val requestIds : List<String>
|
||||
)
|
||||
val selectedSlots = mutableListOf<PreparedCoverageSlot>()
|
||||
val cycleBegin = Duration.between(tnCalc, timeStart).toDays() / durationCalc
|
||||
val cycleEnd = Duration.between(tnCalc, timeStop).toDays() / durationCalc
|
||||
val activeBookedSlots = bookedSlots.filter { it.status in ACTIVE_BOOKED_STATUSES }
|
||||
val booked = activeBookedSlots.map { BookedInfo(it.slot.slotId?:0, it.cycle) }
|
||||
val holderIntervals = activeBookedSlots.map { bookedSlot ->
|
||||
val offsetDays = bookedSlot.cycle * durationCalc
|
||||
HolderInterval(
|
||||
bookedSlot.slot.tn.plusDays(offsetDays),
|
||||
bookedSlot.slot.tk.plusDays(offsetDays),
|
||||
bookedSlot.slot.roll,
|
||||
bookedSlot.requests.orEmpty().map { it.requestId }.distinct()
|
||||
)
|
||||
}
|
||||
val bookedData = activeBookedSlots.map { bookedSlot ->
|
||||
val offsetDays = bookedSlot.cycle * durationCalc
|
||||
BookedSlot(
|
||||
@@ -94,11 +112,27 @@ class TestSatelliteImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Для BUSY-слота собираем держателей: активные брони, чей сдвинутый интервал
|
||||
// пересекается со слотом (с учётом mmi) — та же логика наложения, что и в band.
|
||||
// distinct по requestId: одну бронь могут держать несколько заявок (множ. держатель).
|
||||
val holders = if (band) {
|
||||
holderIntervals
|
||||
.filter { holder ->
|
||||
intersection(it.tn.minusSeconds(mmi), it.tk.plusSeconds(mmi), holder.start, holder.end)
|
||||
}
|
||||
.flatMap { holder -> holder.requestIds }
|
||||
.distinct()
|
||||
.map { reqId -> SlotHolderDTO(reqId) }
|
||||
.takeIf { holders -> holders.isNotEmpty() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
PreparedCoverageSlot(
|
||||
sourceSlotId = it.slotId ?: 0L,
|
||||
slot = it.toDTO(
|
||||
isBooked,
|
||||
band
|
||||
band,
|
||||
holders
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
+27
@@ -9,6 +9,7 @@ import reactor.core.publisher.Flux
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.EarthCellDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.RequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.RequestWithCellsDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotHolderDTO
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
@@ -67,6 +68,32 @@ class EarthService(
|
||||
return Flux.fromIterable(requests)
|
||||
}
|
||||
|
||||
/**
|
||||
* Имя/важность заявок-держателей по идентификаторам (M3 — обзор конфликтов BUSY-слотов).
|
||||
* Один батч-проход по списку заявок (summary уже несёт name+importance — без N+1 на деталь).
|
||||
* Деградация: при недоступности request-service возвращаем пустую карту — бейдж покажет
|
||||
* только идентификатор заявки, без падения request-cover.
|
||||
*/
|
||||
fun requestHolders(ids: Set<String>): Map<String, SlotHolderDTO> {
|
||||
if (ids.isEmpty()) return emptyMap()
|
||||
return runCatching {
|
||||
val firstPage = requestsPage(page = 0)
|
||||
val items = firstPage.items.toMutableList()
|
||||
for (page in 1 until firstPage.totalPages) {
|
||||
items += requestsPage(page).items
|
||||
}
|
||||
items.asSequence()
|
||||
.filter { ids.contains(it.id.toString()) }
|
||||
.associate { summary ->
|
||||
summary.id.toString() to SlotHolderDTO(
|
||||
requestId = summary.id.toString(),
|
||||
name = summary.name.takeIf { it.isNotBlank() },
|
||||
importance = summary.importance,
|
||||
)
|
||||
}
|
||||
}.getOrElse { emptyMap() }
|
||||
}
|
||||
|
||||
private fun requestsPage(page: Int): RequestsListResponseDto =
|
||||
webClientBuilder.build()
|
||||
.get()
|
||||
|
||||
+20
-1
@@ -1892,7 +1892,7 @@ class SlotService {
|
||||
logger.info("Запрос покрытия заявки $id (includeBusy=$includeBusy)")
|
||||
val req = earthService.reqcells(id)?.request?.contour ?: throw CustomErrorException("Нет такой заявки!!!!")
|
||||
logger.info("Получена заявка : $req")
|
||||
return polySlots(
|
||||
val slots = polySlots(
|
||||
req,
|
||||
timeStart,
|
||||
timeStop,
|
||||
@@ -1905,6 +1905,25 @@ class SlotService {
|
||||
coverageStrategy,
|
||||
includeBusy
|
||||
)
|
||||
return if (includeBusy) enrichHolders(slots) else slots
|
||||
}
|
||||
|
||||
// Обогащает держателей BUSY-слотов именем/важностью заявки одним батч-запросом (M3 — обзор
|
||||
// конфликтов). prepareCoverageSlots проставляет держателям только requestId; здесь добавляем
|
||||
// name/importance. Деградация (request-service недоступен) — держатель остаётся с одним id.
|
||||
private fun enrichHolders(slots: Iterable<SlotDTO>): Iterable<SlotDTO> {
|
||||
val slotList = slots.toList()
|
||||
val holderIds = slotList
|
||||
.flatMap { it.holders.orEmpty() }
|
||||
.map { it.requestId }
|
||||
.toSet()
|
||||
if (holderIds.isEmpty()) return slotList
|
||||
val infoById = earthService.requestHolders(holderIds)
|
||||
if (infoById.isEmpty()) return slotList
|
||||
return slotList.map { slot ->
|
||||
val holders = slot.holders ?: return@map slot
|
||||
slot.copy(holders = holders.map { infoById[it.requestId] ?: it })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+47
@@ -3,10 +3,13 @@ package space.nstart.pcp.slots_service.model.satellite
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
||||
import space.nstart.pcp.slots_service.entity.BookedRequestEntity
|
||||
import space.nstart.pcp.slots_service.entity.BookedSlotEntity
|
||||
import space.nstart.pcp.slots_service.entity.SlotEntity
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TestSatelliteImplBookedBlockingTest {
|
||||
|
||||
@@ -40,6 +43,50 @@ class TestSatelliteImplBookedBlockingTest {
|
||||
assertEquals(SlotStatus.BUSY, result.getValue(2L).state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `busy slot carries holders from blocking booking, booked and available slots carry none`() {
|
||||
val baseTime = LocalDateTime.of(2026, 5, 21, 0, 0)
|
||||
val satellite = TestSatelliteImpl(
|
||||
id = 1L,
|
||||
tnCalc = baseTime,
|
||||
durationCalc = 1,
|
||||
maxChainLength = 3,
|
||||
maxSurveyDuration = 300,
|
||||
mmi = 10
|
||||
)
|
||||
val bookedRawSlot = slot(slotId = 1L, slotNum = 1L, baseTime = baseTime, startSecond = 0, endSecond = 10)
|
||||
val candidateInsideBooked = slot(slotId = 2L, slotNum = 2L, baseTime = baseTime, startSecond = 2, endSecond = 8)
|
||||
// Свободный кандидат вне интервала и mmi-окна брони — держателей быть не должно.
|
||||
val freeSlot = slot(slotId = 3L, slotNum = 3L, baseTime = baseTime, startSecond = 100, endSecond = 110)
|
||||
// Множественный держатель: одну бронь держат две заявки.
|
||||
val bookedWithRequests = BookedSlotEntity(
|
||||
slot = bookedRawSlot,
|
||||
cycle = 0,
|
||||
requests = mutableListOf(
|
||||
BookedRequestEntity(requestId = "req-A"),
|
||||
BookedRequestEntity(requestId = "req-B"),
|
||||
),
|
||||
status = BookedSlotStatus.PROCESSED.name,
|
||||
)
|
||||
|
||||
val result = satellite.prepareCoverageSlots(
|
||||
slots = listOf(bookedRawSlot, candidateInsideBooked, freeSlot),
|
||||
bookedSlots = listOf(bookedWithRequests),
|
||||
timeStart = baseTime,
|
||||
timeStop = baseTime.plusSeconds(120),
|
||||
sign = null,
|
||||
states = null
|
||||
).map { it.slot }.associateBy { it.slotNumber }
|
||||
|
||||
val busy = result.getValue(2L)
|
||||
assertEquals(SlotStatus.BUSY, busy.state)
|
||||
assertEquals(listOf("req-A", "req-B"), busy.holders.orEmpty().map { it.requestId }.sorted())
|
||||
// На уровне прогноза держателям проставлен только requestId — name/importance добавит reqCover.
|
||||
assertTrue(busy.holders.orEmpty().all { it.name == null && it.importance == null })
|
||||
assertNull(result.getValue(1L).holders, "у самой брони (BOOKED) держателей не показываем")
|
||||
assertNull(result.getValue(3L).holders, "у свободного слота держателей нет")
|
||||
}
|
||||
|
||||
private fun slot(
|
||||
slotId: Long,
|
||||
slotNum: Long,
|
||||
|
||||
Reference in New Issue
Block a user