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:
Дмитрий Соловьев
2026-06-04 00:03:52 +03:00
parent 0e534c5bd5
commit 3638afdb85
57 changed files with 230 additions and 1063 deletions
@@ -2,7 +2,6 @@ package space.nstart.pcp.pcp_satellites_service.dto
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
import java.time.LocalDateTime
import java.time.OffsetDateTime
data class ComplexPlanRunResponseDTO(
val runId: Long,
@@ -11,9 +10,9 @@ data class ComplexPlanRunResponseDTO(
val intervalEnd: LocalDateTime,
val satelliteIds: List<Long>,
val requestedBy: String?,
val createdAt: OffsetDateTime,
val startedAt: OffsetDateTime?,
val finishedAt: OffsetDateTime?,
val createdAt: LocalDateTime,
val startedAt: LocalDateTime?,
val finishedAt: LocalDateTime?,
val durationMs: Long?,
val errorMessage: String?,
val calculatedModesCount: Int,
@@ -2,7 +2,6 @@ package space.nstart.pcp.pcp_satellites_service.dto
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
import java.time.LocalDateTime
import java.time.OffsetDateTime
data class ComplexPlanRunSummaryDTO(
val runId: Long,
@@ -11,9 +10,9 @@ data class ComplexPlanRunSummaryDTO(
val intervalEnd: LocalDateTime,
val satelliteIds: List<Long>,
val requestedBy: String?,
val createdAt: OffsetDateTime,
val startedAt: OffsetDateTime?,
val finishedAt: OffsetDateTime?,
val createdAt: LocalDateTime,
val startedAt: LocalDateTime?,
val finishedAt: LocalDateTime?,
val durationMs: Long?,
val errorMessage: String?,
val calculatedModesCount: Int,
@@ -4,9 +4,6 @@ import org.springframework.stereotype.Component
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunResponseDTO
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunSummaryDTO
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunEntity
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
@Component
class ComplexPlanRunMapper {
@@ -19,9 +16,9 @@ class ComplexPlanRunMapper {
intervalEnd = entity.intervalEnd,
satelliteIds = satelliteIds(entity),
requestedBy = entity.requestedBy,
createdAt = entity.createdAt.toUtcOffset(),
startedAt = entity.startedAt?.toUtcOffset(),
finishedAt = entity.finishedAt?.toUtcOffset(),
createdAt = entity.createdAt,
startedAt = entity.startedAt,
finishedAt = entity.finishedAt,
durationMs = entity.durationMs,
errorMessage = entity.errorMessage,
calculatedModesCount = entity.calculatedModesCount,
@@ -46,9 +43,9 @@ class ComplexPlanRunMapper {
intervalEnd = entity.intervalEnd,
satelliteIds = satelliteIds(entity),
requestedBy = entity.requestedBy,
createdAt = entity.createdAt.toUtcOffset(),
startedAt = entity.startedAt?.toUtcOffset(),
finishedAt = entity.finishedAt?.toUtcOffset(),
createdAt = entity.createdAt,
startedAt = entity.startedAt,
finishedAt = entity.finishedAt,
durationMs = entity.durationMs,
errorMessage = entity.errorMessage,
calculatedModesCount = entity.calculatedModesCount,
@@ -75,8 +72,4 @@ class ComplexPlanRunMapper {
.mapNotNull { it.id }
.distinct()
.sorted()
private fun LocalDateTime.toUtcOffset(): OffsetDateTime =
atOffset(ZoneOffset.UTC)
}
@@ -50,9 +50,9 @@ class ComplexPlanControllerTest {
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
satelliteIds = listOf(22L),
requestedBy = "tester",
createdAt = LocalDateTime.of(2026, 3, 26, 9, 59).atOffset(ZoneOffset.UTC),
startedAt = LocalDateTime.of(2026, 3, 26, 10, 0).atOffset(ZoneOffset.UTC),
finishedAt = LocalDateTime.of(2026, 3, 26, 10, 1).atOffset(ZoneOffset.UTC),
createdAt = LocalDateTime.of(2026, 3, 26, 9, 59),
startedAt = LocalDateTime.of(2026, 3, 26, 10, 0),
finishedAt = LocalDateTime.of(2026, 3, 26, 10, 1),
durationMs = 60000,
errorMessage = null,
calculatedModesCount = 1,
@@ -22,9 +22,9 @@ import space.nstart.pcp.pcp_satellites_service.service.ComplexPlanRunPersistence
import space.nstart.pcp.pcp_satellites_service.service.SatelliteModePersistenceService
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ComplexPlanRunApiTest {
@@ -81,7 +81,7 @@ class ComplexPlanRunApiTest {
assertEquals(10.0, response.totalRequestsArea)
assertEquals(4.0, response.coveredArea)
assertEquals(40.0, response.coveredAreaPercent)
assertEquals(ZoneOffset.UTC, response.createdAt.offset)
assertNotNull(response.createdAt)
}
@Test
@@ -16,7 +16,6 @@ import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
import space.nstart.pcp.pcp_types_lib.dto.requests.ComplexPlanProcessRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@@ -191,9 +190,9 @@ class ComplexPlanRunOrchestrationServiceTest {
intervalEnd = request.intervalEnd,
satelliteIds = listOf(22L),
requestedBy = "tester",
createdAt = request.intervalStart.minusMinutes(1).atOffset(ZoneOffset.UTC),
startedAt = request.intervalStart.atOffset(ZoneOffset.UTC),
finishedAt = request.intervalStart.plusMinutes(1).atOffset(ZoneOffset.UTC),
createdAt = request.intervalStart.minusMinutes(1),
startedAt = request.intervalStart,
finishedAt = request.intervalStart.plusMinutes(1),
durationMs = 60_000,
errorMessage = null,
calculatedModesCount = 1,
@@ -301,9 +300,9 @@ class ComplexPlanRunOrchestrationServiceTest {
intervalEnd = request.intervalEnd,
satelliteIds = listOf(22L),
requestedBy = "tester",
createdAt = request.intervalStart.minusMinutes(1).atOffset(ZoneOffset.UTC),
startedAt = request.intervalStart.atOffset(ZoneOffset.UTC),
finishedAt = request.intervalStart.plusMinutes(1).atOffset(ZoneOffset.UTC),
createdAt = request.intervalStart.minusMinutes(1),
startedAt = request.intervalStart,
finishedAt = request.intervalStart.plusMinutes(1),
durationMs = 60_000,
errorMessage = null,
calculatedModesCount = 0,