Формат времени в UTC
This commit is contained in:
+28
@@ -1,15 +1,24 @@
|
||||
package space.nstart.pcp_tgu_service.config
|
||||
|
||||
/** Настраивает Jackson-сериализацию для payload'ов клиента Camunda. */
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.JsonSerializer
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import io.camunda.client.api.JsonMapper as CamundaJsonMapper
|
||||
import io.camunda.client.impl.CamundaObjectMapper
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Configuration
|
||||
class CamundaJacksonConfig {
|
||||
@@ -24,6 +33,7 @@ class CamundaJacksonConfig {
|
||||
return JsonMapper.builder()
|
||||
.addModule(KotlinModule.Builder().build())
|
||||
.addModule(JavaTimeModule())
|
||||
.addModule(jackson2UtcLocalDateTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
.build()
|
||||
}
|
||||
@@ -34,3 +44,21 @@ class CamundaJacksonConfig {
|
||||
return CamundaObjectMapper(objectMapper)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun jackson2UtcLocalDateTimeModule(): SimpleModule = SimpleModule("pcp-tgu-service-jackson2-utc-local-date-time")
|
||||
.addSerializer(LocalDateTime::class.java, Jackson2UtcLocalDateTimeSerializer())
|
||||
.addDeserializer(LocalDateTime::class.java, Jackson2UtcLocalDateTimeDeserializer())
|
||||
|
||||
private class Jackson2UtcLocalDateTimeSerializer : JsonSerializer<LocalDateTime>() {
|
||||
override fun serialize(value: LocalDateTime, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeString(UtcDateTimes.format(value))
|
||||
}
|
||||
}
|
||||
|
||||
private class Jackson2UtcLocalDateTimeDeserializer : JsonDeserializer<LocalDateTime>() {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): LocalDateTime {
|
||||
val value = p.valueAsString ?: throw IllegalArgumentException("Expected ISO-8601 UTC date-time string")
|
||||
return UtcDateTimes.parse(value)
|
||||
}
|
||||
}
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package space.nstart.pcp_tgu_service.config
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.convert.converter.Converter
|
||||
import org.springframework.format.FormatterRegistry
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
import tools.jackson.core.JsonGenerator
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import tools.jackson.databind.SerializationContext
|
||||
import tools.jackson.databind.deser.std.StdDeserializer
|
||||
import tools.jackson.databind.module.SimpleModule
|
||||
import tools.jackson.databind.ser.std.StdSerializer
|
||||
import java.time.Clock
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* UTC contract for REST JSON and query parameters.
|
||||
*
|
||||
* Domain/JPA models still keep LocalDateTime as UTC-naive values. External API accepts explicit
|
||||
* UTC/offset strings and legacy naive strings, normalizes offsets to UTC, and writes responses
|
||||
* as ISO-8601 UTC strings with Z.
|
||||
*/
|
||||
@Configuration
|
||||
class UtcTimeConfiguration : WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
fun clock(): Clock = Clock.systemUTC()
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(WebClient.Builder::class)
|
||||
fun webClientBuilder(): WebClient.Builder = WebClient.builder()
|
||||
|
||||
@Bean
|
||||
fun tguUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder ->
|
||||
builder.addModule(
|
||||
SimpleModule("pcp-tgu-service-utc-local-date-time")
|
||||
.addSerializer(LocalDateTime::class.java, UtcLocalDateTimeSerializer())
|
||||
.addDeserializer(LocalDateTime::class.java, UtcLocalDateTimeDeserializer())
|
||||
)
|
||||
}
|
||||
|
||||
override fun addFormatters(registry: FormatterRegistry) {
|
||||
registry.addConverter(UtcLocalDateTimeQueryParamConverter())
|
||||
}
|
||||
}
|
||||
|
||||
private class UtcLocalDateTimeQueryParamConverter : Converter<String, LocalDateTime> {
|
||||
override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source)
|
||||
}
|
||||
|
||||
private class UtcLocalDateTimeSerializer : StdSerializer<LocalDateTime>(LocalDateTime::class.java) {
|
||||
override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) {
|
||||
gen.writeString(UtcDateTimes.format(value))
|
||||
}
|
||||
}
|
||||
|
||||
private class UtcLocalDateTimeDeserializer : StdDeserializer<LocalDateTime>(LocalDateTime::class.java) {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): LocalDateTime {
|
||||
val value = p.valueAsString ?: throw IllegalArgumentException("Expected ISO-8601 UTC date-time string")
|
||||
return UtcDateTimes.parse(value)
|
||||
}
|
||||
}
|
||||
+2
-7
@@ -1,7 +1,6 @@
|
||||
package space.nstart.pcp_tgu_service.controller
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.format.annotation.DateTimeFormat
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
@@ -90,12 +89,8 @@ class TestTguController(
|
||||
@GetMapping("/external-points/{noradId}")
|
||||
fun getExternalPoints(
|
||||
@PathVariable noradId: Long,
|
||||
@RequestParam("time_start")
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
||||
timeStart: LocalDateTime,
|
||||
@RequestParam("time_stop")
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
||||
timeStop: LocalDateTime
|
||||
@RequestParam("time_start") timeStart: LocalDateTime,
|
||||
@RequestParam("time_stop") timeStop: LocalDateTime
|
||||
): String =
|
||||
externalPointsClient.fetchRawVisibilityPayload(noradId, timeStart, timeStop)
|
||||
|
||||
|
||||
+4
-4
@@ -10,8 +10,8 @@ import java.util.UUID
|
||||
/**
|
||||
* DTO ответа REST API для плана.
|
||||
*
|
||||
* Времена ([startTime]/[endTime]) — naive `LocalDateTime`, сериализуются как ISO-строка без зоны
|
||||
* (напр. `"2026-05-29T01:00:00"`). Хранение, домен и API работают на одних настенных часах.
|
||||
* Времена ([startTime]/[endTime]) — UTC-naive `LocalDateTime` в домене/БД.
|
||||
* На REST-границе сериализуются как ISO-8601 UTC с `Z` (например `"2026-05-29T01:00:00Z"`).
|
||||
*/
|
||||
data class PlanResponse(
|
||||
|
||||
@@ -21,10 +21,10 @@ data class PlanResponse(
|
||||
/** Идентификатор космического аппарата */
|
||||
val spacecraftId: String,
|
||||
|
||||
/** Время начала плана (ISO без зоны). */
|
||||
/** Время начала плана (UTC). */
|
||||
val startTime: LocalDateTime,
|
||||
|
||||
/** Время окончания плана (ISO без зоны). */
|
||||
/** Время окончания плана (UTC). */
|
||||
val endTime: LocalDateTime,
|
||||
|
||||
/** Идентификатор пункта закладки */
|
||||
|
||||
+3
-2
@@ -8,6 +8,7 @@ import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Persisted job на запуск короткого BPMN-процесса обновления слотов accepted plan.
|
||||
@@ -39,10 +40,10 @@ class AcceptedPlanSlotsUpdateJobEntity(
|
||||
var lastError: String? = null,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
var createdAt: LocalDateTime = LocalDateTime.now(),
|
||||
var createdAt: LocalDateTime = UtcDateTimes.now(),
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: LocalDateTime = LocalDateTime.now()
|
||||
var updatedAt: LocalDateTime = UtcDateTimes.now()
|
||||
) {
|
||||
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
|
||||
constructor() : this(planId = UUID.randomUUID())
|
||||
|
||||
+3
-2
@@ -8,6 +8,7 @@ import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Одна попытка выдачи persisted plan через один BPMN process instance.
|
||||
@@ -45,10 +46,10 @@ class PlanIssueAttemptEntity(
|
||||
var finishedAt: LocalDateTime? = null,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
var createdAt: LocalDateTime = LocalDateTime.now(),
|
||||
var createdAt: LocalDateTime = UtcDateTimes.now(),
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: LocalDateTime = LocalDateTime.now()
|
||||
var updatedAt: LocalDateTime = UtcDateTimes.now()
|
||||
) {
|
||||
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
|
||||
constructor() : this(attemptId = UUID.randomUUID(), planId = UUID.randomUUID(), attemptNo = 0)
|
||||
|
||||
+3
-2
@@ -8,6 +8,7 @@ import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Persisted business-plan с устойчивым planId по unique business key.
|
||||
@@ -56,10 +57,10 @@ class PlannedPlanEntity(
|
||||
var dismissedAt: LocalDateTime? = null,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
var createdAt: LocalDateTime = LocalDateTime.now(),
|
||||
var createdAt: LocalDateTime = UtcDateTimes.now(),
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: LocalDateTime = LocalDateTime.now()
|
||||
var updatedAt: LocalDateTime = UtcDateTimes.now()
|
||||
) {
|
||||
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
|
||||
constructor() : this(
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Dedup-запись Kafka decision event по eventId.
|
||||
@@ -18,7 +19,7 @@ class ProcessedPlanDecisionEventEntity(
|
||||
var eventId: UUID,
|
||||
|
||||
@Column(name = "received_at", nullable = false)
|
||||
var receivedAt: LocalDateTime = LocalDateTime.now()
|
||||
var receivedAt: LocalDateTime = UtcDateTimes.now()
|
||||
) {
|
||||
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
|
||||
constructor() : this(eventId = UUID.randomUUID())
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
@Entity
|
||||
@Table(name = "spacecraft_planning_state")
|
||||
@@ -42,7 +43,7 @@ class SpacecraftPlanningStateEntity(
|
||||
var pausedAfterReject: Boolean = false,
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: LocalDateTime = LocalDateTime.now()
|
||||
var updatedAt: LocalDateTime = UtcDateTimes.now()
|
||||
) {
|
||||
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
|
||||
constructor() : this(spacecraftId = "")
|
||||
|
||||
+3
-2
@@ -5,6 +5,7 @@ import jakarta.persistence.Entity
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import java.time.LocalDateTime
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Persisted snapshot КА из НСИ, используемый как вход для refresh jobs.
|
||||
@@ -44,10 +45,10 @@ class SpacecraftSnapshotEntity(
|
||||
var active: Boolean = true,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
var createdAt: LocalDateTime = LocalDateTime.now(),
|
||||
var createdAt: LocalDateTime = UtcDateTimes.now(),
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: LocalDateTime = LocalDateTime.now()
|
||||
var updatedAt: LocalDateTime = UtcDateTimes.now()
|
||||
) {
|
||||
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
|
||||
constructor() : this(spacecraftId = "", noradId = 0)
|
||||
|
||||
+3
-2
@@ -5,6 +5,7 @@ import jakarta.persistence.Entity
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import java.time.LocalDateTime
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Полный raw snapshot окон видимости КА из баллистики.
|
||||
@@ -40,10 +41,10 @@ class SpacecraftVisibilitySnapshotEntity(
|
||||
var windowsCount: Int? = null,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
var createdAt: LocalDateTime = LocalDateTime.now(),
|
||||
var createdAt: LocalDateTime = UtcDateTimes.now(),
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: LocalDateTime = LocalDateTime.now()
|
||||
var updatedAt: LocalDateTime = UtcDateTimes.now()
|
||||
) {
|
||||
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
|
||||
constructor() : this(spacecraftId = "", noradId = 0, payloadJson = "", payloadHash = "", fetchedAt = LocalDateTime.MIN)
|
||||
|
||||
+3
-2
@@ -7,6 +7,7 @@ import jakarta.persistence.Enumerated
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import java.time.LocalDateTime
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Job на перечитывание окон видимости одного КА у баллистики.
|
||||
@@ -41,10 +42,10 @@ class VisibilityRefreshJobEntity(
|
||||
var rerunRequested: Boolean = false,
|
||||
|
||||
@Column(name = "requested_at", nullable = false)
|
||||
var requestedAt: LocalDateTime = LocalDateTime.now(),
|
||||
var requestedAt: LocalDateTime = UtcDateTimes.now(),
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: LocalDateTime = LocalDateTime.now()
|
||||
var updatedAt: LocalDateTime = UtcDateTimes.now()
|
||||
) {
|
||||
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
|
||||
constructor() : this(spacecraftId = "")
|
||||
|
||||
+6
-4
@@ -13,6 +13,7 @@ import org.springframework.web.reactive.function.client.WebClientException
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException
|
||||
import java.time.LocalDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Клиент внешнего API с окнами видимости КА.
|
||||
@@ -20,6 +21,7 @@ import java.time.temporal.ChronoUnit
|
||||
@Component
|
||||
class ExternalPointsClient(
|
||||
externalApiProperties: ExternalApiProperties,
|
||||
webClientBuilder: WebClient.Builder,
|
||||
private val platformService: PlatformService,
|
||||
private val planningProperties: PlanningProperties,
|
||||
private val visibilityPayloadParser: VisibilityPayloadParser
|
||||
@@ -27,7 +29,7 @@ class ExternalPointsClient(
|
||||
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
private val webClient: WebClient = WebClient.builder()
|
||||
private val webClient: WebClient = webClientBuilder
|
||||
.baseUrl(externalApiProperties.baseUrl)
|
||||
.build()
|
||||
|
||||
@@ -36,7 +38,7 @@ class ExternalPointsClient(
|
||||
* которые разрешены для работы TGU.
|
||||
*/
|
||||
fun fetchAllSpacecraftPoints(): List<SpacecraftPoints> {
|
||||
val timeStart = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES)
|
||||
val timeStart = UtcDateTimes.now().truncatedTo(ChronoUnit.MINUTES)
|
||||
val timeStop = timeStart.plusDays(planningProperties.externalPointsLookaheadDays)
|
||||
|
||||
return platformService.loadAllowedPlatforms()
|
||||
@@ -93,8 +95,8 @@ class ExternalPointsClient(
|
||||
.uri { uriBuilder ->
|
||||
uriBuilder
|
||||
.path("/api/satellites/{satelliteId}/rva")
|
||||
.queryParam("time_start", timeStart)
|
||||
.queryParam("time_stop", timeStop)
|
||||
.queryParam("time_start", UtcDateTimes.format(timeStart))
|
||||
.queryParam("time_stop", UtcDateTimes.format(timeStop))
|
||||
.build(satelliteId)
|
||||
}
|
||||
.retrieve()
|
||||
|
||||
+33
-5
@@ -4,6 +4,7 @@ import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
import space.nstart.pcp_tgu_service.config.MissionPlaningProperties
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
@@ -23,10 +24,25 @@ data class MissionBlankRequest(
|
||||
val missionStop: LocalDateTime
|
||||
)
|
||||
|
||||
@Component
|
||||
class MissionPlaningClient(properties: MissionPlaningProperties) {
|
||||
private data class MissionCloneOutboundRequest(
|
||||
val mode: String,
|
||||
val windowStart: String? = null,
|
||||
val windowStop: String? = null
|
||||
)
|
||||
|
||||
private val webClient: WebClient = WebClient.builder()
|
||||
private data class MissionBlankOutboundRequest(
|
||||
val satelliteId: Long,
|
||||
val missionStart: String,
|
||||
val missionStop: String
|
||||
)
|
||||
|
||||
@Component
|
||||
class MissionPlaningClient(
|
||||
properties: MissionPlaningProperties,
|
||||
webClientBuilder: WebClient.Builder
|
||||
) {
|
||||
|
||||
private val webClient: WebClient = webClientBuilder
|
||||
.baseUrl(properties.baseUrl)
|
||||
.build()
|
||||
|
||||
@@ -35,7 +51,7 @@ class MissionPlaningClient(properties: MissionPlaningProperties) {
|
||||
fun cloneMission(missionId: UUID, req: MissionCloneRequest): MissionCloneResponse =
|
||||
webClient.post()
|
||||
.uri("/api/missions/{id}/clone", missionId)
|
||||
.bodyValue(req)
|
||||
.bodyValue(req.toOutbound())
|
||||
.retrieve()
|
||||
.onStatus({ it.isError }) { resp ->
|
||||
resp.bodyToMono(String::class.java).defaultIfEmpty("").map { body ->
|
||||
@@ -53,11 +69,23 @@ class MissionPlaningClient(properties: MissionPlaningProperties) {
|
||||
"Пустой ответ mission-planing на клон миссии $missionId"
|
||||
)
|
||||
|
||||
private fun MissionCloneRequest.toOutbound(): MissionCloneOutboundRequest = MissionCloneOutboundRequest(
|
||||
mode = mode,
|
||||
windowStart = windowStart?.let(UtcDateTimes::format),
|
||||
windowStop = windowStop?.let(UtcDateTimes::format)
|
||||
)
|
||||
|
||||
private fun MissionBlankRequest.toOutbound(): MissionBlankOutboundRequest = MissionBlankOutboundRequest(
|
||||
satelliteId = satelliteId,
|
||||
missionStart = UtcDateTimes.format(missionStart),
|
||||
missionStop = UtcDateTimes.format(missionStop)
|
||||
)
|
||||
|
||||
/** Создаёт пустую миссию на окно (план новой цепочки «без истории»). */
|
||||
fun createBlankMission(req: MissionBlankRequest): MissionCloneResponse =
|
||||
webClient.post()
|
||||
.uri("/api/missions/blank")
|
||||
.bodyValue(req)
|
||||
.bodyValue(req.toOutbound())
|
||||
.retrieve()
|
||||
.onStatus({ it.isError }) { resp ->
|
||||
resp.bodyToMono(String::class.java).defaultIfEmpty("").map { body ->
|
||||
|
||||
+5
-17
@@ -6,10 +6,8 @@ import space.nstart.pcp_tgu_service.domain.InsertionPoint
|
||||
import space.nstart.pcp_tgu_service.domain.ObservationWindow
|
||||
import space.nstart.pcp_tgu_service.domain.SpacecraftPoints
|
||||
import space.nstart.pcp_tgu_service.dto.ExternalRvaItemDto
|
||||
import java.time.Instant
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
/**
|
||||
@@ -43,21 +41,11 @@ class VisibilityPayloadParser(
|
||||
}
|
||||
|
||||
/**
|
||||
* Разбирает ISO-строку времени баллистики в naive `LocalDateTime`.
|
||||
* Разбирает ISO-строку времени баллистики в UTC-naive `LocalDateTime`.
|
||||
*
|
||||
* Если время приходит с зоной (`...Z`/офсет), момент проецируем в локальный пояс JVM
|
||||
* (`ZoneId.systemDefault()`): naive-время в системе — настенные часы локальной зоны, в которой
|
||||
* работают все сервисы, без UTC-семантики. Naive-строку без зоны берём как есть.
|
||||
*
|
||||
* Обрезаем до микросекунд — это эффективная точность системы: `planned_plan.start_time/end_time`
|
||||
* хранятся как Postgres `timestamp(6)`, при записи наносекундный хвост баллистики всё равно теряется.
|
||||
* Без обрезки проекция (наносекунды) и persisted-планы (микросекунды) перестают совпадать по
|
||||
* бизнес-ключу — ломается дедуп в read-пути и матчинг базы в [calculatePlansAfter].
|
||||
* Явные UTC/offset-строки нормализуются в UTC, legacy naive-строки считаются UTC-naive.
|
||||
* Обрезаем до микросекунд — это эффективная точность persisted-планов в БД.
|
||||
*/
|
||||
private fun toLocalDateTime(value: String): LocalDateTime =
|
||||
try {
|
||||
LocalDateTime.ofInstant(Instant.parse(value), ZoneId.systemDefault())
|
||||
} catch (_: DateTimeParseException) {
|
||||
LocalDateTime.parse(value)
|
||||
}.truncatedTo(ChronoUnit.MICROS)
|
||||
UtcDateTimes.parse(value).truncatedTo(ChronoUnit.MICROS)
|
||||
}
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import org.springframework.transaction.annotation.Transactional
|
||||
import space.nstart.pcp_tgu_service.repository.AcceptedPlanSlotsUpdateJobRepository
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Создаёт persisted job обновления слотов для accepted plan.
|
||||
@@ -16,6 +17,6 @@ class AcceptedPlanSlotsUpdateJobService(
|
||||
/** Idempotent enqueue: один planId получает максимум одну job. */
|
||||
@Transactional
|
||||
fun enqueue(planId: UUID) {
|
||||
acceptedPlanSlotsUpdateJobRepository.insertPendingIfAbsent(planId, LocalDateTime.now())
|
||||
acceptedPlanSlotsUpdateJobRepository.insertPendingIfAbsent(planId, UtcDateTimes.now())
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -11,6 +11,7 @@ import space.nstart.pcp_tgu_service.repository.AcceptedPlanSlotsUpdateJobReposit
|
||||
import java.net.InetAddress
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Scale-out safe worker, который запускает updateAcceptedPlanSlots для accepted plan.
|
||||
@@ -35,7 +36,7 @@ class AcceptedPlanSlotsUpdateWorker(
|
||||
/** Claim видим для тестов; в production защищён FOR UPDATE SKIP LOCKED. */
|
||||
fun claimJobs(limit: Int): List<AcceptedPlanSlotsUpdateJobEntity> =
|
||||
transactionTemplate.execute {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
val jobs = acceptedPlanSlotsUpdateJobRepository.findClaimableForUpdate(now, limit)
|
||||
jobs.forEach { job ->
|
||||
job.status = AcceptedPlanSlotsUpdateJobStatus.RUNNING
|
||||
@@ -69,7 +70,7 @@ class AcceptedPlanSlotsUpdateWorker(
|
||||
|
||||
private fun markDone(planId: UUID, processInstanceKey: Long) {
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
val job = acceptedPlanSlotsUpdateJobRepository.findById(planId).orElse(null) ?: return@executeWithoutResult
|
||||
job.status = AcceptedPlanSlotsUpdateJobStatus.DONE
|
||||
job.processInstanceKey = processInstanceKey
|
||||
@@ -83,7 +84,7 @@ class AcceptedPlanSlotsUpdateWorker(
|
||||
|
||||
private fun markAmbiguous(planId: UUID, processInstanceKey: Long?, ex: Exception) {
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
val job = acceptedPlanSlotsUpdateJobRepository.findById(planId).orElse(null) ?: return@executeWithoutResult
|
||||
job.status = AcceptedPlanSlotsUpdateJobStatus.AMBIGUOUS
|
||||
job.processInstanceKey = processInstanceKey ?: job.processInstanceKey
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
|
||||
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Создаёт план новой цепочки (вкладка «Планы», действие оператора «Создать план») и сразу считает
|
||||
@@ -46,7 +47,7 @@ class CreateChainPlanUseCase(
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
fun createChainPlan(req: CreateChainPlanRequest): CreateChainPlanResponse {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
|
||||
if (!req.windowStop.isAfter(req.windowStart)) {
|
||||
throw ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "windowStop должен быть позже windowStart")
|
||||
|
||||
+2
-1
@@ -16,6 +16,7 @@ import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
|
||||
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Создаёт черновик плана (DRAFT) на основе существующего базового плана.
|
||||
@@ -45,7 +46,7 @@ class CreateDraftUseCase(
|
||||
val base = plannedPlanRepository.findById(basePlanId)
|
||||
.orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Plan $basePlanId not found") }
|
||||
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
|
||||
return when (req.mode) {
|
||||
DraftMode.REPLACE -> createReplaceDraft(base, now)
|
||||
|
||||
+4
-3
@@ -9,6 +9,7 @@ import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
|
||||
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* «Снять с учёта» / «Вернуть на учёт» терминально-негативный план.
|
||||
@@ -44,8 +45,8 @@ class DismissPlanUseCase(
|
||||
log.info("План {} уже снят с учёта ({}) — идемпотентно", planId, plan.dismissedAt)
|
||||
return
|
||||
}
|
||||
plan.dismissedAt = LocalDateTime.now()
|
||||
plan.updatedAt = LocalDateTime.now()
|
||||
plan.dismissedAt = UtcDateTimes.now()
|
||||
plan.updatedAt = UtcDateTimes.now()
|
||||
plannedPlanRepository.save(plan)
|
||||
log.info("План {} (status={}) снят с учёта", planId, plan.status)
|
||||
}
|
||||
@@ -59,7 +60,7 @@ class DismissPlanUseCase(
|
||||
return
|
||||
}
|
||||
plan.dismissedAt = null
|
||||
plan.updatedAt = LocalDateTime.now()
|
||||
plan.updatedAt = UtcDateTimes.now()
|
||||
plannedPlanRepository.save(plan)
|
||||
log.info("План {} (status={}) возвращён на учёт", planId, plan.status)
|
||||
}
|
||||
|
||||
+7
-6
@@ -20,6 +20,7 @@ import space.nstart.pcp_tgu_service.repository.ProcessedPlanDecisionEventReposit
|
||||
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Обрабатывает decision и закладочные feedback-события идемпотентно по eventId.
|
||||
@@ -132,7 +133,7 @@ class HandlePlanDecisionUseCase(
|
||||
)
|
||||
}
|
||||
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
plan.status = PlannedPlanStatus.LAID_IN
|
||||
plan.updatedAt = now
|
||||
|
||||
@@ -213,7 +214,7 @@ class HandlePlanDecisionUseCase(
|
||||
return
|
||||
}
|
||||
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
plan.status = terminalStatus
|
||||
plan.updatedAt = now
|
||||
plannedPlanRepository.save(plan)
|
||||
@@ -225,7 +226,7 @@ class HandlePlanDecisionUseCase(
|
||||
message: PlanDecisionMessage,
|
||||
attempt: PlanIssueAttemptEntity
|
||||
) {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
attempt.status = when (message.decision) {
|
||||
PlanDecision.ACCEPTED -> PlanIssueAttemptStatus.ACCEPTED
|
||||
PlanDecision.REJECTED -> PlanIssueAttemptStatus.REJECTED
|
||||
@@ -241,7 +242,7 @@ class HandlePlanDecisionUseCase(
|
||||
attempt: PlanIssueAttemptEntity,
|
||||
plan: PlannedPlanEntity
|
||||
) {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
recordDecisionOnAttempt(message, attempt)
|
||||
|
||||
plan.status = PlannedPlanStatus.AWAITING_LAYIN
|
||||
@@ -262,7 +263,7 @@ class HandlePlanDecisionUseCase(
|
||||
plan: PlannedPlanEntity,
|
||||
state: SpacecraftPlanningStateEntity
|
||||
) {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
recordDecisionOnAttempt(message, attempt)
|
||||
|
||||
plan.status = PlannedPlanStatus.REJECTED
|
||||
@@ -307,7 +308,7 @@ class HandlePlanDecisionUseCase(
|
||||
}
|
||||
|
||||
private fun claimEvent(eventId: java.util.UUID): Boolean =
|
||||
processedPlanDecisionEventRepository.insertIfAbsent(eventId, LocalDateTime.now()) != 0
|
||||
processedPlanDecisionEventRepository.insertIfAbsent(eventId, UtcDateTimes.now()) != 0
|
||||
|
||||
private fun getOrCreateLockedState(spacecraftId: String): SpacecraftPlanningStateEntity {
|
||||
spacecraftPlanningStateJpaRepository.findById(spacecraftId).orElse(null)
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import space.nstart.pcp_tgu_service.dto.StationParamsChangedMessage
|
||||
import space.nstart.pcp_tgu_service.repository.ProcessedPlanDecisionEventRepository
|
||||
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
|
||||
import java.time.LocalDateTime
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* На изменение параметров станций перезапрашивает ЗРВ у баллистики по всем активным КА.
|
||||
@@ -25,7 +26,7 @@ class HandleStationParamsChangedUseCase(
|
||||
/** Идемпотентно по eventId; ставит refresh job по всем активным КА. */
|
||||
@Transactional
|
||||
fun handle(message: StationParamsChangedMessage) {
|
||||
val claimed = processedPlanDecisionEventRepository.insertIfAbsent(message.eventId, LocalDateTime.now()) != 0
|
||||
val claimed = processedPlanDecisionEventRepository.insertIfAbsent(message.eventId, UtcDateTimes.now()) != 0
|
||||
if (!claimed) {
|
||||
return
|
||||
}
|
||||
|
||||
+6
-5
@@ -20,6 +20,7 @@ import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaReposit
|
||||
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Выдаёт дозревшие планы. В on-demand-модели голова цепочки считается из снапшота ЗРВ
|
||||
@@ -83,7 +84,7 @@ class IssueDuePlanWorker(
|
||||
|
||||
/** Готовит выдачу головы проекции для КА под lock'ом состояния (scale-out safe). */
|
||||
private fun prepareDueForSpacecraft(spacecraftId: String): PreparedAttempt? {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
val state = getOrCreateLockedState(spacecraftId)
|
||||
// Форвард-цепочку не выдаём, пока по КА есть выдача «в работе»: ISSUING или WAITING_DECISION
|
||||
// в пределах grace после старта. Прошедший grace больше не блокирует — молчаливо продолжаем
|
||||
@@ -154,7 +155,7 @@ class IssueDuePlanWorker(
|
||||
.maxByOrNull { plan -> plan.endTime }?.planId ?: fallback
|
||||
|
||||
private fun prepareSpecificPlan(planId: UUID): PreparedAttempt? {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
val plan = plannedPlanRepository.findById(planId).orElse(null) ?: return null
|
||||
val state = getOrCreateLockedState(plan.spacecraftId)
|
||||
assertReissuableInSlot(plan, now)
|
||||
@@ -176,7 +177,7 @@ class IssueDuePlanWorker(
|
||||
}
|
||||
|
||||
private fun prepareDraftPromotion(draftPlanId: UUID): PreparedAttempt? {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
val plan = plannedPlanRepository.findById(draftPlanId).orElse(null) ?: return null
|
||||
if (plan.status != PlannedPlanStatus.DRAFT) {
|
||||
throw ResponseStatusException(
|
||||
@@ -326,7 +327,7 @@ class IssueDuePlanWorker(
|
||||
|
||||
private fun markStarted(response: ProcessStartResponse) {
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
val attempt = planIssueAttemptRepository.findById(response.attemptId).orElse(null) ?: return@executeWithoutResult
|
||||
val plan = plannedPlanRepository.findById(response.planId).orElse(null) ?: return@executeWithoutResult
|
||||
|
||||
@@ -350,7 +351,7 @@ class IssueDuePlanWorker(
|
||||
|
||||
private fun markAmbiguous(preparedAttempt: PreparedAttempt) {
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
val attempt = planIssueAttemptRepository.findById(preparedAttempt.attemptId).orElse(null) ?: return@executeWithoutResult
|
||||
val plan = plannedPlanRepository.findById(preparedAttempt.planId).orElse(null) ?: return@executeWithoutResult
|
||||
attempt.status = PlanIssueAttemptStatus.AMBIGUOUS
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
|
||||
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Read-сервис планов: объединяет persisted (выданные, история — вечно) с вычисленной
|
||||
@@ -80,7 +81,7 @@ class PlanQueryService(
|
||||
.sortedBy { response -> response.startTime }
|
||||
|
||||
private fun historySince(historyDays: Long): LocalDateTime =
|
||||
LocalDateTime.now().minusDays(historyDays.coerceAtLeast(0))
|
||||
UtcDateTimes.now().minusDays(historyDays.coerceAtLeast(0))
|
||||
|
||||
private fun PlannedPlanEntity.businessKey(): String =
|
||||
"$spacecraftId|$startTime|$endTime|$kppId"
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import space.nstart.pcp_tgu_service.integration.api.VisibilityPayloadParser
|
||||
import space.nstart.pcp_tgu_service.repository.SpacecraftVisibilitySnapshotRepository
|
||||
import java.security.MessageDigest
|
||||
import java.time.LocalDateTime
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Сохраняет raw visibility payload. Расписание считается on-demand из снапшота
|
||||
@@ -25,7 +26,7 @@ class SpacecraftVisibilitySnapshotService(
|
||||
val payloadHash = sha256(payloadJson)
|
||||
val existing = spacecraftVisibilitySnapshotRepository.findById(spacecraftId).orElse(null)
|
||||
val metadata = runCatching { extractMetadata(spacecraftId, payloadJson) }.getOrNull()
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
|
||||
if (existing == null) {
|
||||
spacecraftVisibilitySnapshotRepository.save(
|
||||
|
||||
+5
-6
@@ -5,7 +5,7 @@ import org.springframework.transaction.support.TransactionTemplate
|
||||
import space.nstart.pcp_tgu_service.domain.PlatformCatalogItem
|
||||
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Синхронизирует разрешённые КА из НСИ в БД и ставит refresh jobs.
|
||||
@@ -26,7 +26,7 @@ class SyncSpacecraftFromNsiUseCase(
|
||||
}
|
||||
|
||||
private fun persistAllowedSpacecraft(allowedPlatforms: List<PlatformCatalogItem>) {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
val allowedSpacecraftIds = allowedPlatforms.mapNotNull { platform -> platform.noradId?.toString() }
|
||||
|
||||
allowedPlatforms.forEach { platform ->
|
||||
@@ -40,10 +40,9 @@ class SyncSpacecraftFromNsiUseCase(
|
||||
name = platform.name,
|
||||
status = platform.status,
|
||||
mission = platform.mission,
|
||||
// validFrom/validTo приходят как Instant; в naive-поля БД пишем настенные часы локальной
|
||||
// зоны JVM (ZoneId.systemDefault()) — naive-время в системе локальное, без UTC-семантики.
|
||||
validFrom = platform.validFrom?.let { LocalDateTime.ofInstant(it, ZoneId.systemDefault()) },
|
||||
validTo = platform.validTo?.let { LocalDateTime.ofInstant(it, ZoneId.systemDefault()) },
|
||||
// validFrom/validTo приходят как Instant; в naive-поля БД пишем UTC-naive момент.
|
||||
validFrom = platform.validFrom?.let { LocalDateTime.ofInstant(it, UtcDateTimes.UTC) },
|
||||
validTo = platform.validTo?.let { LocalDateTime.ofInstant(it, UtcDateTimes.UTC) },
|
||||
now = now
|
||||
)
|
||||
visibilityRefreshJobService.enqueue(spacecraftId, noradId, "NSI_SYNC")
|
||||
|
||||
+3
-2
@@ -12,6 +12,7 @@ import space.nstart.pcp_tgu_service.repository.PlanIssueAttemptRepository
|
||||
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Test-only helper: генерирует decision event для последней (живой) attempt persisted plan.
|
||||
@@ -50,7 +51,7 @@ class TestPlanDecisionService(
|
||||
planId = plan.planId,
|
||||
attemptId = attempt.attemptId,
|
||||
decision = decision,
|
||||
decisionTime = LocalDateTime.now(),
|
||||
decisionTime = UtcDateTimes.now(),
|
||||
reason = reason?.takeIf { it.isNotBlank() } ?: DEFAULT_REASON
|
||||
)
|
||||
handlePlanDecisionUseCase.handle(message)
|
||||
@@ -70,7 +71,7 @@ class TestPlanDecisionService(
|
||||
eventId = UUID.randomUUID(),
|
||||
spacecraftId = plan.spacecraftId,
|
||||
planId = plan.planId,
|
||||
occurredAt = LocalDateTime.now(),
|
||||
occurredAt = UtcDateTimes.now(),
|
||||
reason = reason?.takeIf { it.isNotBlank() } ?: DEFAULT_REASON
|
||||
)
|
||||
handlePlanDecisionUseCase.handleLayinConfirmed(message)
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import space.nstart.pcp_tgu_service.repository.VisibilityRefreshJobRepository
|
||||
import java.time.LocalDateTime
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Coalesce-сервис для visibility_refresh_job по spacecraftId.
|
||||
@@ -15,6 +16,6 @@ class VisibilityRefreshJobService(
|
||||
/** Ставит или переоткрывает refresh job без дублей. */
|
||||
@Transactional
|
||||
fun enqueue(spacecraftId: String, noradId: Long?, reason: String) {
|
||||
visibilityRefreshJobRepository.upsertPending(spacecraftId, noradId, reason, LocalDateTime.now())
|
||||
visibilityRefreshJobRepository.upsertPending(spacecraftId, noradId, reason, UtcDateTimes.now())
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -13,6 +13,7 @@ import space.nstart.pcp_tgu_service.repository.VisibilityRefreshJobRepository
|
||||
import java.net.InetAddress
|
||||
import java.time.LocalDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
|
||||
/**
|
||||
* Claim'ит visibility_refresh_job, читает raw payload у баллистики и сохраняет snapshot.
|
||||
@@ -38,7 +39,7 @@ class VisibilityRefreshWorker(
|
||||
|
||||
private fun claimJobs(limit: Int): List<VisibilityRefreshJobEntity> =
|
||||
transactionTemplate.execute {
|
||||
val now = LocalDateTime.now()
|
||||
val now = UtcDateTimes.now()
|
||||
val jobs = visibilityRefreshJobRepository.findPendingForUpdate(now, limit)
|
||||
jobs.forEach { job ->
|
||||
job.status = JobStatus.RUNNING
|
||||
@@ -61,10 +62,10 @@ class VisibilityRefreshWorker(
|
||||
}
|
||||
|
||||
try {
|
||||
val timeStart = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES)
|
||||
val timeStart = UtcDateTimes.now().truncatedTo(ChronoUnit.MINUTES)
|
||||
val timeStop = timeStart.plusDays(planningProperties.externalPointsLookaheadDays)
|
||||
val payload = externalPointsClient.fetchRawVisibilityPayload(noradId, timeStart, timeStop)
|
||||
spacecraftVisibilitySnapshotService.saveSnapshot(spacecraftId, noradId, payload, LocalDateTime.now())
|
||||
spacecraftVisibilitySnapshotService.saveSnapshot(spacecraftId, noradId, payload, UtcDateTimes.now())
|
||||
markDone(spacecraftId)
|
||||
} catch (ex: Exception) {
|
||||
log.warn("Visibility refresh job failed. spacecraftId={}, noradId={}", spacecraftId, noradId, ex)
|
||||
@@ -79,7 +80,7 @@ class VisibilityRefreshWorker(
|
||||
job.rerunRequested = false
|
||||
job.lockedBy = null
|
||||
job.lockedUntil = null
|
||||
job.updatedAt = LocalDateTime.now()
|
||||
job.updatedAt = UtcDateTimes.now()
|
||||
visibilityRefreshJobRepository.save(job)
|
||||
}
|
||||
}
|
||||
@@ -90,7 +91,7 @@ class VisibilityRefreshWorker(
|
||||
job.status = if (job.attempts >= MAX_ATTEMPTS) JobStatus.FAILED else JobStatus.PENDING
|
||||
job.lockedBy = null
|
||||
job.lockedUntil = null
|
||||
job.updatedAt = LocalDateTime.now()
|
||||
job.updatedAt = UtcDateTimes.now()
|
||||
visibilityRefreshJobRepository.save(job)
|
||||
}
|
||||
}
|
||||
@@ -102,7 +103,7 @@ class VisibilityRefreshWorker(
|
||||
job.reason = reason
|
||||
job.lockedBy = null
|
||||
job.lockedUntil = null
|
||||
job.updatedAt = LocalDateTime.now()
|
||||
job.updatedAt = UtcDateTimes.now()
|
||||
visibilityRefreshJobRepository.save(job)
|
||||
}
|
||||
}
|
||||
|
||||
+18
-6
@@ -1,25 +1,37 @@
|
||||
package space.nstart.pcp_tgu_service.dto
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.JsonSerializer
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
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.Test
|
||||
import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
|
||||
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Контракт формата времени на проводе для [PlanResponse]: naive `LocalDateTime` → ISO без зоны
|
||||
* (напр. `"2026-05-29T01:00:00"`). Тест ловит случайный возврат к OffsetDateTime/Z или числовым меткам.
|
||||
* Контракт формата времени на проводе для [PlanResponse]: UTC-naive `LocalDateTime` → ISO UTC с Z
|
||||
* (напр. `"2026-05-29T01:00:00Z"`). Тест ловит случайный возврат к naive-строкам или числовым меткам.
|
||||
*/
|
||||
class PlanResponseContractTest {
|
||||
|
||||
private val mapper = jacksonObjectMapper().findAndRegisterModules()
|
||||
.registerModule(
|
||||
SimpleModule().addSerializer(LocalDateTime::class.java, object : JsonSerializer<LocalDateTime>() {
|
||||
override fun serialize(value: LocalDateTime, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeString(UtcDateTimes.format(value))
|
||||
}
|
||||
})
|
||||
)
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
|
||||
@Test
|
||||
fun `serializes plan times as naive ISO without zone`() {
|
||||
fun `serializes plan times as UTC ISO with Z`() {
|
||||
val response = PlanResponse(
|
||||
planId = UUID.fromString("00000000-0000-0000-0000-000000000001"),
|
||||
spacecraftId = "25544",
|
||||
@@ -31,8 +43,8 @@ class PlanResponseContractTest {
|
||||
|
||||
val node = mapper.readTree(mapper.writeValueAsString(response))
|
||||
|
||||
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")
|
||||
assertEquals("2026-05-29T01:00:00Z", node.get("startTime").asText())
|
||||
assertEquals("2026-05-29T02:00:00Z", node.get("endTime").asText())
|
||||
assertFalse(node.get("startTime").asText().contains("["), "не ожидался zoned date-time с ID зоны")
|
||||
}
|
||||
}
|
||||
|
||||
+4
-6
@@ -19,9 +19,8 @@ class VisibilityPayloadParserTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `projects zoned instant into JVM default zone as naive local LocalDateTime`() {
|
||||
// naive-время в системе — настенные часы локальной зоны JVM (без UTC-семантики). Парсер проецирует
|
||||
// зонированный инстант через ZoneId.systemDefault(), поэтому на МСК (+3) 14:14Z даёт локальные 17:14.
|
||||
fun `normalizes zoned instant to UTC naive LocalDateTime`() {
|
||||
// Зонированный инстант должен нормализоваться в UTC-naive время независимо от зоны JVM.
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Moscow"))
|
||||
val payload = """
|
||||
[{
|
||||
@@ -35,9 +34,8 @@ class VisibilityPayloadParserTest {
|
||||
|
||||
val window = parser.parse("20", payload).points.single().observationWindows.single()
|
||||
|
||||
// Инстант 14:14Z спроецирован в локальную зону JVM (МСК, +3) — настенные 17:14.
|
||||
assertEquals(LocalDateTime.parse("2026-06-02T17:14:08"), window.startTime)
|
||||
assertEquals(LocalDateTime.parse("2026-06-02T17:20:00"), window.endTime)
|
||||
assertEquals(LocalDateTime.parse("2026-06-02T14:14:08"), window.startTime)
|
||||
assertEquals(LocalDateTime.parse("2026-06-02T14:20:00"), window.endTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user