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
@@ -6,34 +6,15 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule
@Configuration
class JacksonConfig {
@Bean
fun objectMapper(): ObjectMapper {
val mapper = ObjectMapper()
// Настраиваем формат для LocalDateTime
// val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.zzz")
// module.addSerializer<LocalDateTime?>(
// LocalDateTime::class.java,
// LocalDateTimeSerializer(formatter)
// )
// module.addDeserializer<LocalDateTime?>(
// LocalDateTime::class.java,
// LocalDateTimeDeserializer(formatter)
// )
mapper.registerModule(JavaTimeModule())
.registerKotlinModule()
// Слой толерантности времени (читать naive=UTC и Z) — см. docs/standards/DATETIME_UTC.md.
// Этот маппер создаётся вручную и не подхватывает модуль ни через Boot-бин, ни через SPI.
.registerModule(PcpTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
return mapper
}
}
@@ -48,6 +48,7 @@ import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
@@ -841,7 +842,7 @@ class BasicPlanCalculator(
val resultFilePath = if (request.saveResultToFile && status == ComplexPlanCalculationStatus.COMPLETED) {
observationParametersDebugFileWriter.write(
ObservationParametersDebugFileDTO(
createdAt = Instant.now(),
createdAt = LocalDateTime.now(ZoneOffset.UTC),
requestId = request.requestId,
satelliteIds = selectedSatelliteIds,
missingSatelliteIds = missingSatelliteIds,
@@ -7,8 +7,7 @@ import org.springframework.web.client.HttpClientErrorException
import org.springframework.web.client.RestClient
import org.springframework.web.util.UriComponentsBuilder
import space.nstart.pcp.complan.types.RequestItemDTO
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.LocalDateTime
import java.util.UUID
/** Получение заявок из pcp-request-service /v1 API. */
@@ -44,10 +43,9 @@ class RequestServiceClient {
id = id,
name = name,
geometry = geometry,
// Нормализуем к UTC: pcp-request-service шлёт `Z`, но голый toLocalDateTime() уехал бы
// при любом ненулевом офсете. Все naive LocalDateTime в системе — UTC (docs/standards/DATETIME_UTC.md).
intervalBegin = beginDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(),
intervalEnd = endDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(),
// pcp-request-service шлёт naive LocalDateTime без зоны — берём как есть (настенные часы UTC).
intervalBegin = beginDateTime,
intervalEnd = endDateTime,
resolution = resolution(),
importance = importance,
)
@@ -66,16 +64,16 @@ data class RequestResponseDto(
val surveyType: String = "",
val geometry: String,
val importance: Double,
val beginDateTime: OffsetDateTime,
val endDateTime: OffsetDateTime,
val beginDateTime: LocalDateTime,
val endDateTime: LocalDateTime,
val kpp: List<Int> = emptyList(),
val highPriorityTransmit: Boolean = false,
val optics: SurveyParamsDto? = null,
val rsa: SurveyParamsDto? = null,
val coverage: CoverageStateDto = CoverageStateDto(),
val createdAt: OffsetDateTime? = null,
val updatedAt: OffsetDateTime? = null,
val deletedAt: OffsetDateTime? = null,
val createdAt: LocalDateTime? = null,
val updatedAt: LocalDateTime? = null,
val deletedAt: LocalDateTime? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -3,7 +3,6 @@ package space.nstart.pcp.complan.types
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import io.swagger.v3.oas.annotations.media.Schema
import java.time.Instant
import java.time.LocalDateTime
import java.util.UUID
@@ -116,7 +115,7 @@ data class ObservationParametersDebugFileDTO(
@get:JsonProperty("createdAt")
@param:JsonProperty("createdAt")
val createdAt: Instant,
val createdAt: LocalDateTime,
@get:JsonProperty("requestId")
@param:JsonProperty("requestId")
@@ -9,7 +9,6 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import java.nio.file.Files
import java.nio.file.Path
import java.time.Instant
import java.time.LocalDateTime
import java.util.UUID
import kotlin.test.Test
@@ -29,7 +28,7 @@ class ObservationParametersDebugFileWriterTest {
outputDir = tempDir.resolve("observation-parameters").toString()
)
val artifact = ObservationParametersDebugFileDTO(
createdAt = Instant.parse("2026-05-05T10:15:30Z"),
createdAt = LocalDateTime.parse("2026-05-05T10:15:30"),
requestId = requestId,
satelliteIds = listOf(1001L),
missingSatelliteIds = emptyList(),
@@ -62,29 +62,11 @@ class RequestServiceClientTest {
assertFalse(requestedUris.any { uri -> uri.path.startsWith("/api/v1/requests") })
}
@Test
fun `getRequest normalizes non-UTC offsets to UTC LocalDateTime`() {
val requestId = UUID.fromString("00000000-0000-0000-0000-000000000002")
// Источник прислал то же мгновение, но со смещением +03:00. Голый toLocalDateTime() оставил бы
// локальную стенку (06:04:05); нормализация к UTC обязана дать 03:04:05.
server = serverWithRequestResponse(
mutableListOf(),
requestId,
beginDateTime = "2026-01-02T06:04:05+03:00",
endDateTime = "2026-01-03T07:05:06+03:00",
)
val request = requestServiceClient().getRequest(requestId)!!
assertEquals(LocalDateTime.of(2026, 1, 2, 3, 4, 5), request.intervalBegin)
assertEquals(LocalDateTime.of(2026, 1, 3, 4, 5, 6), request.intervalEnd)
}
private fun serverWithRequestResponse(
requestedUris: MutableList<URI>,
requestId: UUID,
beginDateTime: String = "2026-01-02T03:04:05Z",
endDateTime: String = "2026-01-03T04:05:06Z",
beginDateTime: String = "2026-01-02T03:04:05",
endDateTime: String = "2026-01-03T04:05:06",
): HttpServer =
HttpServer.create(InetSocketAddress(0), 0).apply {
createContext("/v1/requests") { exchange ->
@@ -110,8 +92,8 @@ class RequestServiceClientTest {
},
"rsa": null,
"coverage": {"currentPercent": 0.0},
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z",
"createdAt": "2026-01-01T00:00:00",
"updatedAt": "2026-01-01T00:00:00",
"deletedAt": null
}
""".trimIndent()