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:
-19
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+9
-11
@@ -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)
|
||||
|
||||
+1
-2
@@ -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")
|
||||
|
||||
+1
-2
@@ -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(),
|
||||
|
||||
+4
-22
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user