pcp: откат UTC-слоя — сквозной naive LocalDateTime без зоны на проводе
Возврат к модели «настенных часов»: одна ISO-строка без зоны
("2026-05-29T01:00:00") от БД до экрана, никто не привязывает её к
часовому поясу, сдвигаться нечему.
Бэкенд — снять зону с границы API:
- DTO OffsetDateTime/Instant -> LocalDateTime и убрана конвертация
atOffset(ZoneOffset.UTC)/toUtcOffset() в фабриках и мапперах:
tgu PlanResponse/NewChainDTOs/TguPlanningDTO, request RequestApiDtos,
complex ComplexPlanRun*DTO, dynamic ObservationParametersDebugDTO.
- Удалён толерантный Jackson-слой: PcpTimeModule(+autoconfig, ServiceLoader,
imports), PcpTimeModuleV3, LocalDateTimeTimeZoneSerializer; снята их
регистрация в Camunda/Rest/dynamic JacksonConfig. Штатный JSR-310
(JavaTimeModule + WRITE_DATES_AS_TIMESTAMPS=false) оставлен.
- Kafka-конверт KafkaMessage.time сериализуется ISO без офсета.
- mission-planing Mode/SurveyMode/DropMode entity и репозитории/сервис
переведены на LocalDateTime; колонки БД остаются timestamptz (Hibernate
мапит на naive без миграции — прецедент MissionEntity/PlanEntity).
- RequestServiceClient (dynamic) синхронизирован по типу с request-service.
Анти-регрессия: LocalDateTime.now(ZoneOffset.UTC) для серверных штампов
оставлен (выбор настенных часов UTC, не зона на проводе).
Фронт — снять офсет с egress: toUtcIso/фильтры отдают naive без Z
(после удаления толерантного десериализатора штатный парсер падал бы на Z).
Дисплейные хелперы (показ «как пришло», без сдвига) сохранены.
Тесты: контрактные ждут naive ISO; удалены UTC-артефакты (нормализация
офсетов) и тесты прослоек. Удалён docs/standards/DATETIME_UTC.md.
Операционно перед выкатом: сервисы катить согласованно; Kafka-топики с
ранее опубликованным офсетом +00:00 дренировать/проиграть.
This commit is contained in:
+5
-10
@@ -27,8 +27,6 @@ import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.Refli
|
||||
import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.SatelliteMissionTypeFactory
|
||||
import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@@ -263,27 +261,24 @@ class MissionCloneTest {
|
||||
val plan = PlanEntity(id = ++planAutoId, mission = mission, planNumber = 1)
|
||||
planStore[plan.id!!] = plan
|
||||
val duration = java.time.Duration.between(surveyStart, surveyStop).toMillis().toDouble() / 1000.0
|
||||
val s1 = survey(plan, surveyStart.atOffset(ZoneOffset.UTC), duration)
|
||||
val s2 = survey(plan, surveyStart.plusMinutes(1).atOffset(ZoneOffset.UTC), duration)
|
||||
val s1 = survey(plan, surveyStart, duration)
|
||||
val s2 = survey(plan, surveyStart.plusMinutes(1), duration)
|
||||
val d = drop(plan, surveyStart.plusMinutes(5))
|
||||
return SourceFixture(mission.missionId, plan, s1, s2, d)
|
||||
}
|
||||
|
||||
private fun survey(plan: PlanEntity, startOffset: OffsetDateTime, duration: Double = 300.0): SurveyModeEntity {
|
||||
private fun survey(plan: PlanEntity, start: LocalDateTime, duration: Double = 300.0): SurveyModeEntity {
|
||||
val id = ++modeAutoId
|
||||
val s = SurveyModeEntity(id = id, plan = plan, timeStart = startOffset, revolution = id,
|
||||
val s = SurveyModeEntity(id = id, plan = plan, timeStart = start, revolution = id,
|
||||
lat = 55.0, long = 37.0, duration = duration, roll = 0.0, source = SurveyModeSource.MANUAL,
|
||||
status = SurveyModeStatus.PLANNED)
|
||||
surveyStore[id] = s
|
||||
return s
|
||||
}
|
||||
|
||||
private fun survey(plan: PlanEntity, start: LocalDateTime, duration: Double = 300.0) =
|
||||
survey(plan, start.atOffset(ZoneOffset.UTC), duration)
|
||||
|
||||
private fun drop(plan: PlanEntity, start: LocalDateTime): DropModeEntity {
|
||||
val id = ++modeAutoId
|
||||
val d = DropModeEntity(id = id, plan = plan, timeStart = start.atOffset(ZoneOffset.UTC),
|
||||
val d = DropModeEntity(id = id, plan = plan, timeStart = start,
|
||||
revolution = id, station = "102", duration = 60.0)
|
||||
dropStore[id] = d
|
||||
return d
|
||||
|
||||
+2
-3
@@ -32,7 +32,6 @@ import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.DropRouteSaveRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SurveyRouteBuildRequestDTO
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
|
||||
class MissionEditModesTest {
|
||||
@@ -262,7 +261,7 @@ class MissionEditModesTest {
|
||||
private fun survey(): SurveyModeEntity {
|
||||
val id = ++autoId
|
||||
val s = SurveyModeEntity(
|
||||
id = id, plan = plan, timeStart = LocalDateTime.of(2026, 6, 10, 12, 0).atOffset(ZoneOffset.UTC),
|
||||
id = id, plan = plan, timeStart = LocalDateTime.of(2026, 6, 10, 12, 0),
|
||||
revolution = id, lat = 55.0, long = 37.0, duration = 60.0, roll = 0.0,
|
||||
status = SurveyModeStatus.PLANNED, source = SurveyModeSource.MANUAL
|
||||
)
|
||||
@@ -273,7 +272,7 @@ class MissionEditModesTest {
|
||||
private fun drop(): DropModeEntity {
|
||||
val id = ++autoId
|
||||
val d = DropModeEntity(
|
||||
id = id, plan = plan, timeStart = LocalDateTime.of(2026, 6, 10, 13, 0).atOffset(ZoneOffset.UTC),
|
||||
id = id, plan = plan, timeStart = LocalDateTime.of(2026, 6, 10, 13, 0),
|
||||
revolution = id, station = "102", duration = 60.0
|
||||
)
|
||||
dropStore[id] = d
|
||||
|
||||
+8
-45
@@ -41,8 +41,6 @@ import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.Refli
|
||||
import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.SatelliteMissionTypeFactory
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
import java.math.BigDecimal
|
||||
|
||||
@@ -93,7 +91,7 @@ class MissionPlaningServiceTest {
|
||||
val overlappingSurvey = SurveyModeEntity(
|
||||
id = 1L,
|
||||
plan = plan,
|
||||
timeStart = intervalStart.minusMinutes(5).atOffset(ZoneOffset.UTC),
|
||||
timeStart = intervalStart.minusMinutes(5),
|
||||
revolution = 10,
|
||||
lat = 55.0,
|
||||
long = 37.0,
|
||||
@@ -105,7 +103,7 @@ class MissionPlaningServiceTest {
|
||||
val inWindowSurvey = SurveyModeEntity(
|
||||
id = 2L,
|
||||
plan = plan,
|
||||
timeStart = intervalStart.plusMinutes(10).atOffset(ZoneOffset.UTC),
|
||||
timeStart = intervalStart.plusMinutes(10),
|
||||
revolution = 11,
|
||||
lat = 56.0,
|
||||
long = 38.0,
|
||||
@@ -117,7 +115,7 @@ class MissionPlaningServiceTest {
|
||||
val inWindowDrop = DropModeEntity(
|
||||
id = 3L,
|
||||
plan = plan,
|
||||
timeStart = intervalStart.plusMinutes(20).atOffset(ZoneOffset.UTC),
|
||||
timeStart = intervalStart.plusMinutes(20),
|
||||
revolution = 12,
|
||||
station = "station-1",
|
||||
duration = 120.0
|
||||
@@ -126,7 +124,7 @@ class MissionPlaningServiceTest {
|
||||
`when`(
|
||||
modeRepository.findAllByPlan_Mission_SatelliteIdAndTimeStartIsNotNullAndTimeStartLessThanEqualOrderByTimeStartAsc(
|
||||
77,
|
||||
intervalStop.atOffset(ZoneOffset.UTC)
|
||||
intervalStop
|
||||
)
|
||||
).thenReturn(listOf(overlappingSurvey, inWindowSurvey, inWindowDrop))
|
||||
`when`(modeBookingRepository.findAllByMode_IdIn(listOf(1L, 2L))).thenReturn(
|
||||
@@ -153,41 +151,6 @@ class MissionPlaningServiceTest {
|
||||
assertEquals(listOf(2L), (result[2] as DropModeResponseDTO).surveys)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `modesBySatelliteAndInterval normalizes non-UTC mode offsets to UTC before matching`() {
|
||||
val mission = MissionEntity(missionId = UUID.randomUUID(), satelliteId = 77)
|
||||
val plan = PlanEntity(id = 101L, mission = mission, planNumber = 1)
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 16, 10, 0, 0)
|
||||
val intervalStop = LocalDateTime.of(2026, 3, 16, 10, 30, 0)
|
||||
|
||||
// То же мгновение, что 10:10 UTC (внутри окна), но выражено со смещением +03:00 → 13:10.
|
||||
// Голый toLocalDateTime() дал бы 13:10 и выкинул мод из окна; нормализация к UTC оставляет его.
|
||||
val offsetSurvey = SurveyModeEntity(
|
||||
id = 1L,
|
||||
plan = plan,
|
||||
timeStart = OffsetDateTime.of(2026, 3, 16, 13, 10, 0, 0, ZoneOffset.ofHours(3)),
|
||||
revolution = 11,
|
||||
lat = 56.0,
|
||||
long = 38.0,
|
||||
duration = 300.0,
|
||||
contourWkt = "POLYGON((0 0,1 0,1 1,0 0))",
|
||||
roll = 1.0,
|
||||
source = SurveyModeSource.SLOT
|
||||
)
|
||||
|
||||
`when`(
|
||||
modeRepository.findAllByPlan_Mission_SatelliteIdAndTimeStartIsNotNullAndTimeStartLessThanEqualOrderByTimeStartAsc(
|
||||
77,
|
||||
intervalStop.atOffset(ZoneOffset.UTC)
|
||||
)
|
||||
).thenReturn(listOf(offsetSurvey))
|
||||
`when`(modeBookingRepository.findAllByMode_IdIn(listOf(1L))).thenReturn(emptyList())
|
||||
|
||||
val result = service.modesBySatelliteAndInterval(77, intervalStart, intervalStop)
|
||||
|
||||
assertEquals(listOf(1L), result.map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `modesBySatelliteAndInterval rejects inverted interval`() {
|
||||
val exception = assertThrows(ResponseStatusException::class.java) {
|
||||
@@ -223,12 +186,12 @@ class MissionPlaningServiceTest {
|
||||
)
|
||||
val exactMode = SurveyModeEntity(
|
||||
id = 10L,
|
||||
timeStart = routePassport.intervalBegin.atOffset(ZoneOffset.UTC),
|
||||
timeStart = routePassport.intervalBegin,
|
||||
revolution = 321
|
||||
)
|
||||
val nearMode = SurveyModeEntity(
|
||||
id = 11L,
|
||||
timeStart = routePassport.intervalBegin.plusSeconds(1).atOffset(ZoneOffset.UTC),
|
||||
timeStart = routePassport.intervalBegin.plusSeconds(1),
|
||||
revolution = 321
|
||||
)
|
||||
|
||||
@@ -236,8 +199,8 @@ class MissionPlaningServiceTest {
|
||||
surveyModeRepository.findAllBySatelliteNumberAndRevolutionAndTimeStartBetween(
|
||||
"22",
|
||||
321,
|
||||
OffsetDateTime.of(routePassport.intervalBegin.minusSeconds(1), ZoneOffset.UTC),
|
||||
OffsetDateTime.of(routePassport.intervalBegin.plusSeconds(1), ZoneOffset.UTC)
|
||||
routePassport.intervalBegin.minusSeconds(1),
|
||||
routePassport.intervalBegin.plusSeconds(1)
|
||||
)
|
||||
).thenReturn(listOf(nearMode, exactMode))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user