pcp: единообразие времени (UTC) по группе сервисов
Фронт (pcp-tgu-ops-ui): - тест-страж utcTime.guard.test.ts: запрещает Date.parse / new Date(string) / toLocale* без timeZone вне utils/utcTime; тестовые фикстуры переведены на parseUtc. Бэкенд: - UTC-now: LocalDateTime.now() -> LocalDateTime.now(ZoneOffset.UTC) по всем сервисам (entity-дефолты, воркеры, use case'ы) и в тестовых фикстурах. - Конвертации к UTC (баги на не-UTC JVM, каждая с регресс-тестом в не-UTC поясе): - tgu: VisibilityPayloadParser, SyncSpacecraftFromNsiUseCase (Instant -> UTC LocalDateTime); - mission-planing: MissionPlaningService (OffsetDateTime -> UTC через toUtcLocalDateTime); - dynamic-plan: RequestServiceClient (OffsetDateTime -> UTC); - slots: SlotRepositoryImpl (getObject(LocalDateTime) вместо getTimestamp().toLocalDateTime()). - Контрактные сериализационные тесты формата на проводе (naive без зоны / Z): PlanResponse (tgu), RequestResponseDto (request, эталон), TguPlanning DTO (ui). - KDoc-инвариант «=UTC» на UI-facing DTO (tgu PlanResponse, ui-service DTO вкладки ТГУ). Документация: docs/standards/DATETIME_UTC.md (соглашение, таблица сервисов, под-задачи на миграцию к Z), docs/standards/CHECKLIST.md (пункт про UTC на API).
This commit is contained in:
+2
-1
@@ -28,6 +28,7 @@ import space.nstart.pcp.slots_service.service.SatelliteCatalogService
|
||||
import space.nstart.pcp.slots_service.service.SatellitePdcmService
|
||||
import space.nstart.pcp.slots_service.service.SlotService
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.time.format.ResolverStyle
|
||||
@@ -103,7 +104,7 @@ class CatalogRestController(
|
||||
|
||||
private fun parseSendInitialConditionsTime(time: String?): LocalDateTime {
|
||||
val normalizedTime = time?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?: return LocalDateTime.now()
|
||||
?: return LocalDateTime.now(ZoneOffset.UTC)
|
||||
|
||||
return try {
|
||||
LocalDateTime.parse(normalizedTime, sendInitialConditionsTimeFormatter)
|
||||
|
||||
+2
@@ -6,6 +6,8 @@ import java.time.LocalDateTime
|
||||
/**
|
||||
* UI DTO for the bookings page. It intentionally lives in ui-service because the page combines
|
||||
* data from slots-service, request-service and satellite-catalog-service without changing their APIs.
|
||||
*
|
||||
* Времена ([LocalDateTime]) — UTC, naive ISO без зоны (см. docs/standards/DATETIME_UTC.md).
|
||||
*/
|
||||
data class BookedSlotViewDTO(
|
||||
val bookedSlotId: Long,
|
||||
|
||||
+4
@@ -2,6 +2,10 @@ package space.nstart.pcp.slots_service.dto.groupstate
|
||||
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/*
|
||||
* Времена ([LocalDateTime]) — UTC, naive ISO без зоны (см. docs/standards/DATETIME_UTC.md).
|
||||
*/
|
||||
|
||||
data class GroupStateAvailabilityDTO(
|
||||
val timeStart: LocalDateTime,
|
||||
val timeStop: LocalDateTime
|
||||
|
||||
+4
@@ -3,6 +3,10 @@ package space.nstart.pcp.slots_service.dto.pdcm
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/*
|
||||
* Времена ([LocalDateTime]) — UTC, naive ISO без зоны (см. docs/standards/DATETIME_UTC.md).
|
||||
*/
|
||||
|
||||
data class SatellitePdcmAvailabilityDTO(
|
||||
val timeStart: LocalDateTime,
|
||||
val timeStop: LocalDateTime
|
||||
|
||||
+5
@@ -4,6 +4,11 @@ import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
/*
|
||||
* Времена в этом файле — UTC (см. docs/standards/DATETIME_UTC.md): `LocalDateTime` сериализуется как
|
||||
* naive ISO без зоны (= UTC), `Instant` — с `Z`. Потребители (фронт через utils/utcTime) парсят как UTC.
|
||||
*/
|
||||
|
||||
data class TguPlatformResponse(
|
||||
val id: String?,
|
||||
val businessKey: String?,
|
||||
|
||||
+4
@@ -2,6 +2,10 @@ package space.nstart.pcp.slots_service.dto.tleanalysis
|
||||
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/*
|
||||
* Времена ([LocalDateTime], в т.ч. epoch) — UTC, naive ISO без зоны (см. docs/standards/DATETIME_UTC.md).
|
||||
*/
|
||||
|
||||
data class TleAnalysisSatelliteDTO(
|
||||
val id: Long,
|
||||
val catalogId: Long? = null,
|
||||
|
||||
+4
@@ -2,6 +2,10 @@ package space.nstart.pcp.slots_service.dto.tlecomparison
|
||||
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/*
|
||||
* Времена ([LocalDateTime], epoch/time) — UTC, naive ISO без зоны (см. docs/standards/DATETIME_UTC.md).
|
||||
*/
|
||||
|
||||
class TleTextRequestDTO(
|
||||
var tle: String = ""
|
||||
)
|
||||
|
||||
+1
-1
@@ -426,7 +426,7 @@ private val docReqCovName ="req_cov_"
|
||||
timeStop: LocalDateTime? = null
|
||||
): Iterable<CZMLEntity> {
|
||||
|
||||
val tstart = timeStart ?: LocalDateTime.now()
|
||||
val tstart = timeStart ?: LocalDateTime.now(ZoneOffset.UTC)
|
||||
val tstop = timeStop ?: tstart.plusDays(1)
|
||||
var vuz = ballisticsService.ascNodes(id, tstart, tstop).collectList().block()
|
||||
vuz = vuz?.filter { it.time >= tstart && it.time <= tstop }?.sortedBy { it.time }
|
||||
|
||||
+3
-2
@@ -20,6 +20,7 @@ import space.nstart.pcp.slots_service.czml.CZMLPolygon
|
||||
import space.nstart.pcp.slots_service.czml.CZMLPosition
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@Service
|
||||
class CoverageSchemeService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
|
||||
@@ -185,8 +186,8 @@ class CoverageSchemeModeRowDTO(
|
||||
val entityId: String = "",
|
||||
val sequence: Int = 0,
|
||||
val satelliteId: Long = 0,
|
||||
val tn: LocalDateTime = LocalDateTime.now(),
|
||||
val tk: LocalDateTime = LocalDateTime.now(),
|
||||
val tn: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
|
||||
val tk: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
|
||||
val roll: Double? = null,
|
||||
val revolutionSign: RevolutionSign? = null,
|
||||
val contour: String = "",
|
||||
|
||||
+3
-2
@@ -7,6 +7,7 @@ import space.nstart.pcp.slots_service.dto.pdcm.SatellitePdcmAvailabilityDTO
|
||||
import space.nstart.pcp.slots_service.dto.pdcm.SatellitePdcmDetailsDTO
|
||||
import space.nstart.pcp.slots_service.dto.pdcm.SatellitePdcmSummaryDTO
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@Service
|
||||
class SatellitePdcmService(
|
||||
@@ -14,7 +15,7 @@ class SatellitePdcmService(
|
||||
private val ballisticsService: BallisticsService
|
||||
) {
|
||||
|
||||
fun satelliteSummaries(): List<SatellitePdcmSummaryDTO> = satelliteSummaries(LocalDateTime.now())
|
||||
fun satelliteSummaries(): List<SatellitePdcmSummaryDTO> = satelliteSummaries(LocalDateTime.now(ZoneOffset.UTC))
|
||||
|
||||
fun satelliteSummaries(referenceTime: LocalDateTime): List<SatellitePdcmSummaryDTO> {
|
||||
val satellites = satelliteCatalogService.allSatellites()
|
||||
@@ -39,7 +40,7 @@ class SatellitePdcmService(
|
||||
}
|
||||
|
||||
fun satelliteDetails(satelliteId: Long): SatellitePdcmDetailsDTO =
|
||||
satelliteDetails(satelliteId, LocalDateTime.now())
|
||||
satelliteDetails(satelliteId, LocalDateTime.now(ZoneOffset.UTC))
|
||||
|
||||
fun satelliteDetails(satelliteId: Long, referenceTime: LocalDateTime): SatellitePdcmDetailsDTO {
|
||||
val satellites = satelliteCatalogService.allSatellites()
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package space.nstart.pcp.slots_service.dto.tgu
|
||||
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Контракт формата времени на проводе для DTO вкладки ТГУ в pcp-ui-service:
|
||||
* naive `LocalDateTime` → ISO без зоны (= UTC), `Instant` → ISO с `Z`
|
||||
* (см. docs/standards/DATETIME_UTC.md). Тест ловит молчаливый дрейф контракта.
|
||||
*
|
||||
* Mapper зеркалит конфиг Spring Boot: JavaTimeModule + disable(WRITE_DATES_AS_TIMESTAMPS).
|
||||
*/
|
||||
class TguPlanningDtoContractTest {
|
||||
|
||||
private val mapper = jacksonObjectMapper().findAndRegisterModules()
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
|
||||
@Test
|
||||
fun `serializes plan times as naive ISO without zone`() {
|
||||
val plan = TguPlanResponse(
|
||||
planId = UUID.fromString("00000000-0000-0000-0000-000000000001"),
|
||||
spacecraftId = "25544",
|
||||
startTime = LocalDateTime.of(2026, 5, 29, 1, 0, 0),
|
||||
endTime = LocalDateTime.of(2026, 5, 29, 2, 0, 0),
|
||||
kppId = "KPP-1",
|
||||
status = "PLANNED"
|
||||
)
|
||||
|
||||
val node = mapper.readTree(mapper.writeValueAsString(plan))
|
||||
|
||||
assertEquals("2026-05-29T01:00:00", node.get("startTime").asText())
|
||||
assertTrue(
|
||||
node.get("startTime").asText().matches(Regex("""\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}""")),
|
||||
"ожидался naive ISO без зоны"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serializes platform validity instants with explicit Z zone`() {
|
||||
val platform = TguPlatformResponse(
|
||||
id = "25544",
|
||||
businessKey = "bk-1",
|
||||
name = "SAT",
|
||||
status = "OPERATIONAL",
|
||||
noradId = 25544L,
|
||||
mission = "obs",
|
||||
validFrom = Instant.parse("2026-05-29T01:00:00Z"),
|
||||
validTo = Instant.parse("2026-12-31T23:00:00Z")
|
||||
)
|
||||
|
||||
val node = mapper.readTree(mapper.writeValueAsString(platform))
|
||||
|
||||
assertEquals("2026-05-29T01:00:00Z", node.get("validFrom").asText())
|
||||
assertTrue(node.get("validTo").asText().endsWith("Z"), "ожидалась явная зона Z")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user