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,13 +2,11 @@ package space.nstart.pcp.slots_service.dto.tgu
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.util.UUID
/*
* Времена в этом файле — UTC (см. docs/standards/DATETIME_UTC.md). Времена плана ([TguPlanResponse]) —
* `OffsetDateTime` с явной зоной `Z` (как отдаёт pcp-tgu-service); `Instant` — тоже с `Z`. `LocalDateTime`
* (напр. [TguPlanDecisionMessage.decisionTime]) — naive ISO без зоны (= UTC). Фронт парсит через utils/utcTime.
* Времена плана ([TguPlanResponse]) — naive `LocalDateTime` без зоны (как отдаёт pcp-tgu-service).
* `Instant` ([TguPlatformResponse.validFrom/validTo]) — с `Z` (как отдаёт НСИ-прокси).
*/
data class TguPlatformResponse(
@@ -25,8 +23,8 @@ data class TguPlatformResponse(
data class TguPlanResponse(
val planId: UUID,
val spacecraftId: String,
val startTime: OffsetDateTime,
val endTime: OffsetDateTime,
val startTime: LocalDateTime,
val endTime: LocalDateTime,
val kppId: String,
val status: String
)
@@ -45,8 +45,6 @@ import space.nstart.pcp.slots_service.service.SlotService
import space.nstart.pcp.slots_service.service.StationService
import space.nstart.pcp.slots_service.service.TguPlanningService
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.UUID
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -212,8 +210,8 @@ class CatalogControllerTest {
TguPlanResponse(
planId = planId,
spacecraftId = "56756",
startTime = OffsetDateTime.of(2026, 5, 29, 10, 0, 0, 0, ZoneOffset.UTC),
endTime = OffsetDateTime.of(2026, 5, 29, 10, 15, 0, 0, ZoneOffset.UTC),
startTime = LocalDateTime.of(2026, 5, 29, 10, 0, 0),
endTime = LocalDateTime.of(2026, 5, 29, 10, 15, 0),
kppId = "KPP-1",
status = "WAITING_DECISION"
)
@@ -3,20 +3,17 @@ 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.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.LocalDateTime
import java.util.UUID
/**
* Контракт формата времени на проводе для DTO вкладки ТГУ в pcp-ui-service:
* времена плана ([TguPlanResponse]) — `OffsetDateTime` с `Z` (как отдаёт pcp-tgu-service),
* `Instant` ([TguPlatformResponse.validFrom]) — тоже с `Z` (см. docs/standards/DATETIME_UTC.md).
* Тест ловит молчаливый дрейф контракта.
*
* Mapper зеркалит конфиг Spring Boot: JavaTimeModule + disable(WRITE_DATES_AS_TIMESTAMPS).
* времена плана ([TguPlanResponse]) — naive `LocalDateTime` без зоны (как отдаёт pcp-tgu-service),
* `Instant` ([TguPlatformResponse.validFrom]) — с `Z` (как отдаёт НСИ-прокси).
*/
class TguPlanningDtoContractTest {
@@ -24,21 +21,21 @@ class TguPlanningDtoContractTest {
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
@Test
fun `serializes plan times as ISO with explicit Z zone`() {
fun `serializes plan times as naive ISO without zone`() {
val plan = TguPlanResponse(
planId = UUID.fromString("00000000-0000-0000-0000-000000000001"),
spacecraftId = "25544",
startTime = OffsetDateTime.of(2026, 5, 29, 1, 0, 0, 0, ZoneOffset.UTC),
endTime = OffsetDateTime.of(2026, 5, 29, 2, 0, 0, 0, ZoneOffset.UTC),
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:00Z", node.get("startTime").asText())
assertEquals("2026-05-29T02:00:00Z", node.get("endTime").asText())
assertTrue(node.get("startTime").asText().endsWith("Z"), "ожидалась явная зона Z")
assertEquals("2026-05-29T01:00:00", node.get("startTime").asText())
assertEquals("2026-05-29T02:00:00", node.get("endTime").asText())
assertFalse(node.get("startTime").asText().endsWith("Z"), "не ожидалась зона Z")
}
@Test