Формат времени в UTC

This commit is contained in:
emelianov
2026-06-25 16:21:17 +03:00
parent d0ec0ef754
commit fe54c095ab
164 changed files with 1847 additions and 634 deletions
@@ -22,7 +22,7 @@ class TLEStepper(str1: String, str2: String, earthType: EarthType) : AbstractSte
private val ascendingNodeCache = mutableMapOf<Int, Double>() private val ascendingNodeCache = mutableMapOf<Int, Double>()
init { init {
baseEpoch = extractUTCMillis(satellite) / 1000.0 + 10800 baseEpoch = extractUTCMillis(satellite) / 1000.0
nextAscendingNodeAfterEpoch = findNextAscendingNodeAfterEpoch() nextAscendingNodeAfterEpoch = findNextAscendingNodeAfterEpoch()
ascendingNodeCache[0] = nextAscendingNodeAfterEpoch ascendingNodeCache[0] = nextAscendingNodeAfterEpoch
} }
@@ -26,9 +26,9 @@ class AstronomerJ2000(var earthType: EarthType) {
var d = tDtTm / 86400.0 var d = tDtTm / 86400.0
var dat = truncate(d) var dat = truncate(d)
var tt = d - dat var tt = d - dat
if (tt < 0.125) { // if (tt < 0.125) {
dat-- // dat--
} // }
var t = (dat - baseDate) / (36525.0) var t = (dat - baseDate) / (36525.0)
var t2 = t * t var t2 = t * t
@@ -69,11 +69,11 @@ class AstronomerJ2000(var earthType: EarthType) {
psi += r psi += r
if (tt < 0.125) { // if (tt < 0.125) {
tt += 0.875 // tt += 0.875
} else { // } else {
tt -= 0.125 // tt -= 0.125
} // }
r = 84381.448e0 - 46.84024e0 * t - 0.00059e0 * t2 + 0.001813e0 * t3 r = 84381.448e0 - 46.84024e0 * t - 0.00059e0 * t2 + 0.001813e0 * t3
r = r * PI / 180.0 / 3600.0 r = r * PI / 180.0 / 3600.0
@@ -172,7 +172,7 @@ class AstronomerJ2000(var earthType: EarthType) {
fun moonCoordinates(tm: Double): Vector3D { fun moonCoordinates(tm: Double): Vector3D {
var d: Double = tm / 86400.0 var d: Double = tm / 86400.0
d = d - 0.125 // d = d - 0.125
val t: Double = (d - baseDate) / 36525.0 // Юлианский период ot 12h 1.1.2000 val t: Double = (d - baseDate) / 36525.0 // Юлианский период ot 12h 1.1.2000
val t2 = t * t val t2 = t * t
val t3 = t2 * t val t3 = t2 * t
@@ -241,8 +241,8 @@ class AstronomerJ2000(var earthType: EarthType) {
} }
fun sunCoordinates(tm: Double): Vector3D { fun sunCoordinates(tm: Double): Vector3D {
var d = tm / 86400.0 var dd = tm / 86400.0
var dd = d - 0.125 // var dd = d - 0.125
var t = (dd - baseDate) / 36525.0 var t = (dd - baseDate) / 36525.0
var t2 = t * t var t2 = t * t
var t3 = t2 * t var t3 = t2 * t
+1 -1
View File
@@ -22,7 +22,7 @@ fun main() {
val tt2 = LocalDateTime.of(2028, 12, 11, 1, 24, 24, 257000000) val tt2 = LocalDateTime.of(2028, 12, 10, 22, 24, 24, 257000000)
val op = OrbitalPoint( val op = OrbitalPoint(
fromDateTime(tt2), fromDateTime(tt2),
@@ -2,11 +2,12 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.DecimalMin import jakarta.validation.constraints.DecimalMin
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
@Schema(description = "Запрос на расчет параметров выхода на заданное время") @Schema(description = "Запрос на расчет параметров выхода на заданное время")
class ExactTimePositionRequestDTO( class ExactTimePositionRequestDTO(
val time : LocalDateTime = LocalDateTime.now(), val time : LocalDateTime = UtcDateTimes.now(),
val timeStop : LocalDateTime? = null, val timeStop : LocalDateTime? = null,
@field:DecimalMin(value = "1", @field:DecimalMin(value = "1",
message = "Шаг не может быть меньше 1 мс") message = "Шаг не может быть меньше 1 мс")
@@ -2,6 +2,7 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
@Schema(description = "Модель данных параметров трассы полета и полосы обзора") @Schema(description = "Модель данных параметров трассы полета и полосы обзора")
@@ -12,7 +13,7 @@ class FlightLineDTO(
required = true, required = true,
nullable = false nullable = false
) )
val time : LocalDateTime = LocalDateTime.now(), val time : LocalDateTime = UtcDateTimes.now(),
@JsonProperty @JsonProperty
@field:Schema( @field:Schema(
description = "Виток" description = "Виток"
@@ -2,6 +2,7 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.validation.constraints.NotEmpty import jakarta.validation.constraints.NotEmpty
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
class IcRvaRequestDTO( class IcRvaRequestDTO(
@@ -13,8 +14,8 @@ class IcRvaRequestDTO(
@field:NotEmpty(message = "Массив станций приема") @field:NotEmpty(message = "Массив станций приема")
val station: StationDTO = StationDTO(), val station: StationDTO = StationDTO(),
val timeStart : LocalDateTime = LocalDateTime.now(), val timeStart : LocalDateTime = UtcDateTimes.now(),
val timeStop : LocalDateTime = LocalDateTime.now() val timeStop : LocalDateTime = UtcDateTimes.now()
) { ) {
} }
@@ -1,10 +1,11 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics package space.nstart.pcp.pcp_types_lib.dto.ballistics
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
class IntervalDTO( class IntervalDTO(
val begin : LocalDateTime = LocalDateTime.now(), val begin : LocalDateTime = UtcDateTimes.now(),
val end : LocalDateTime = LocalDateTime.now(), val end : LocalDateTime = UtcDateTimes.now(),
val durationSec : Double = 0.0 val durationSec : Double = 0.0
) { ) {
} }
@@ -1,11 +1,12 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics package space.nstart.pcp.pcp_types_lib.dto.ballistics
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
class ObjViewRequestDTO( class ObjViewRequestDTO(
val satellites : List<Long> = listOf(), val satellites : List<Long> = listOf(),
val timeStart : LocalDateTime = LocalDateTime.now(), val timeStart : LocalDateTime = UtcDateTimes.now(),
val timeStop : LocalDateTime = LocalDateTime.now(), val timeStop : LocalDateTime = UtcDateTimes.now(),
val obj : ObjDTO = ObjDTO(), val obj : ObjDTO = ObjDTO(),
val sunAngleMin : Double? = null, val sunAngleMin : Double? = null,
val rollMin : Double? = null, val rollMin : Double? = null,
@@ -1,9 +1,10 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics package space.nstart.pcp.pcp_types_lib.dto.ballistics
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
class OrbPointDTO( class OrbPointDTO(
val time : LocalDateTime = LocalDateTime.now(), val time : LocalDateTime = UtcDateTimes.now(),
val revolution : Long = 0, val revolution : Long = 0,
val vx : Double = 0.0, val vx : Double = 0.0,
val vy : Double = 0.0, val vy : Double = 0.0,
@@ -1,11 +1,12 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics package space.nstart.pcp.pcp_types_lib.dto.ballistics
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
class PointViewParamDTO( class PointViewParamDTO(
val noradId : Long = 0, val noradId : Long = 0,
val objectId : String = "", val objectId : String = "",
val time : LocalDateTime = LocalDateTime.now(), val time : LocalDateTime = UtcDateTimes.now(),
val revolution : Long = 0, val revolution : Long = 0,
val gamma : Double = 0.0, val gamma : Double = 0.0,
val sunAngle : Double = 0.0, val sunAngle : Double = 0.0,
@@ -1,10 +1,11 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics package space.nstart.pcp.pcp_types_lib.dto.ballistics
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
class SatelliteOrbitAvailabilityDTO( class SatelliteOrbitAvailabilityDTO(
val satelliteId: Long = 0, val satelliteId: Long = 0,
val timeStart: LocalDateTime = LocalDateTime.now(), val timeStart: LocalDateTime = UtcDateTimes.now(),
val timeStop: LocalDateTime = LocalDateTime.now() val timeStop: LocalDateTime = UtcDateTimes.now()
) { ) {
} }
@@ -1,5 +1,6 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics package space.nstart.pcp.pcp_types_lib.dto.ballistics
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -8,10 +9,10 @@ class SquareViewParamDTO(
val noradId : Long = 0, val noradId : Long = 0,
val objectId : String = "", val objectId : String = "",
val revolutionBegin : Long = 0, val revolutionBegin : Long = 0,
val timeBegin : LocalDateTime = LocalDateTime.now(), val timeBegin : LocalDateTime = UtcDateTimes.now(),
val revSignBegin : RevolutionSign = RevolutionSign.ASC, val revSignBegin : RevolutionSign = RevolutionSign.ASC,
val revolutionEnd: Long = 0, val revolutionEnd: Long = 0,
val timeEnd : LocalDateTime = LocalDateTime.now(), val timeEnd : LocalDateTime = UtcDateTimes.now(),
val revSignEnd : RevolutionSign = RevolutionSign.ASC, val revSignEnd : RevolutionSign = RevolutionSign.ASC,
val gammaMin : Double = 0.0, val gammaMin : Double = 0.0,
val gammaMax : Double = 0.0 val gammaMax : Double = 0.0
@@ -1,12 +1,13 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics package space.nstart.pcp.pcp_types_lib.dto.ballistics
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
@Schema(description = "Модель данных целеуказаний") @Schema(description = "Модель данных целеуказаний")
class TargetPositionDTO( class TargetPositionDTO(
val time : LocalDateTime = LocalDateTime.now(), val time : LocalDateTime = UtcDateTimes.now(),
val elevation : Double = 0.0, val elevation : Double = 0.0,
val azimuth : Double = 0.0, val azimuth : Double = 0.0,
val range : Double = 0.0 val range : Double = 0.0
@@ -1,8 +1,9 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics package space.nstart.pcp.pcp_types_lib.dto.ballistics
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
class TlePointRequestDTO( class TlePointRequestDTO(
var tle: TLEDTO = TLEDTO(), var tle: TLEDTO = TLEDTO(),
var time: LocalDateTime = LocalDateTime.now() var time: LocalDateTime = UtcDateTimes.now()
) )
@@ -2,6 +2,7 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.validation.constraints.NotEmpty import jakarta.validation.constraints.NotEmpty
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
class TleRvaRequestDTO( class TleRvaRequestDTO(
@@ -11,9 +12,9 @@ class TleRvaRequestDTO(
@field:NotEmpty(message = "Массив станций приема") @field:NotEmpty(message = "Массив станций приема")
val station: StationDTO = StationDTO(), val station: StationDTO = StationDTO(),
val timeStart : LocalDateTime = LocalDateTime.now(), val timeStart : LocalDateTime = UtcDateTimes.now(),
val timeStop : LocalDateTime = LocalDateTime.now() val timeStop : LocalDateTime = UtcDateTimes.now()
) { ) {
} }
@@ -1,11 +1,12 @@
package space.nstart.pcp.pcp_types_lib.dto.coverage package space.nstart.pcp.pcp_types_lib.dto.coverage
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
class CoverageCandidateDebugDTO( class CoverageCandidateDebugDTO(
val satelliteId: Long = 0, val satelliteId: Long = 0,
val timeStart: LocalDateTime = LocalDateTime.now(), val timeStart: LocalDateTime = UtcDateTimes.now(),
val timeEnd: LocalDateTime = LocalDateTime.now(), val timeEnd: LocalDateTime = UtcDateTimes.now(),
val rollDegrees: Double = 0.0, val rollDegrees: Double = 0.0,
val accepted: Boolean = false, val accepted: Boolean = false,
val reason: String = "", val reason: String = "",
@@ -1,12 +1,13 @@
package space.nstart.pcp.pcp_types_lib.dto.coverage package space.nstart.pcp.pcp_types_lib.dto.coverage
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
class CoverageSchemeCalculateRequestDTO( class CoverageSchemeCalculateRequestDTO(
val polygonWkt: String = "", val polygonWkt: String = "",
val satelliteIds: List<Long> = listOf(), val satelliteIds: List<Long> = listOf(),
val timeStart: LocalDateTime = LocalDateTime.now(), val timeStart: LocalDateTime = UtcDateTimes.now(),
val timeEnd: LocalDateTime = LocalDateTime.now(), val timeEnd: LocalDateTime = UtcDateTimes.now(),
val rollStepDegrees: Double? = null, val rollStepDegrees: Double? = null,
val minimumTechnologyOverlap: Double? = null, val minimumTechnologyOverlap: Double? = null,
val strictContinuousCoverage: Boolean = true, val strictContinuousCoverage: Boolean = true,
@@ -1,10 +1,11 @@
package space.nstart.pcp.pcp_types_lib.dto.requests package space.nstart.pcp.pcp_types_lib.dto.requests
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
class EarthCellCalculationRequestDTO( class EarthCellCalculationRequestDTO(
val timeStart : LocalDateTime = LocalDateTime.now(), val timeStart : LocalDateTime = UtcDateTimes.now(),
val timeStop : LocalDateTime = LocalDateTime.now(), val timeStop : LocalDateTime = UtcDateTimes.now(),
val cell : EarthCellWithRequestsDTO = EarthCellWithRequestsDTO() val cell : EarthCellWithRequestsDTO = EarthCellWithRequestsDTO()
) { ) {
} }
@@ -1,12 +1,13 @@
package space.nstart.pcp.pcp_types_lib.dto.requests.slots package space.nstart.pcp.pcp_types_lib.dto.requests.slots
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
data class BookedSlotsStatusChangedEvent( data class BookedSlotsStatusChangedEvent(
val bookedSlotIds: List<Long> = emptyList(), val bookedSlotIds: List<Long> = emptyList(),
val status: BookedSlotStatus = BookedSlotStatus.BOOKED, val status: BookedSlotStatus = BookedSlotStatus.BOOKED,
val occurredAt: LocalDateTime = LocalDateTime.now(), val occurredAt: LocalDateTime = UtcDateTimes.now(),
val sourceService: String = "", val sourceService: String = "",
val missionId: UUID? = null, val missionId: UUID? = null,
val modeId: Long? = null, val modeId: Long? = null,
@@ -1,6 +1,7 @@
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
/** /**
@@ -23,10 +24,10 @@ data class DropRouteSaveRequestDTO(
val revolution: Long = 0, val revolution: Long = 0,
@field:Schema(description = "Начало зоны радиовидимости (локальное время без зоны)") @field:Schema(description = "Начало зоны радиовидимости (локальное время без зоны)")
val zoneStart: LocalDateTime = LocalDateTime.now(), val zoneStart: LocalDateTime = UtcDateTimes.now(),
@field:Schema(description = "Конец зоны радиовидимости (локальное время без зоны)") @field:Schema(description = "Конец зоны радиовидимости (локальное время без зоны)")
val zoneStop: LocalDateTime = LocalDateTime.now(), val zoneStop: LocalDateTime = UtcDateTimes.now(),
@field:Schema(description = "Идентификаторы режимов съёмки (survey_modes), которые сбрасываются в этой зоне") @field:Schema(description = "Идентификаторы режимов съёмки (survey_modes), которые сбрасываются в этой зоне")
val surveyModeIds: List<Long> = emptyList(), val surveyModeIds: List<Long> = emptyList(),
@@ -3,6 +3,7 @@ package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.DecimalMin import jakarta.validation.constraints.DecimalMin
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
/** /**
@@ -22,7 +23,7 @@ data class SurveyRouteBuildRequestDTO(
val satelliteId: Long = 0, val satelliteId: Long = 0,
@field:Schema(description = "Момент видимости (локальное время без зоны)") @field:Schema(description = "Момент видимости (локальное время без зоны)")
val time: LocalDateTime = LocalDateTime.now(), val time: LocalDateTime = UtcDateTimes.now(),
@field:DecimalMin(value = "1", message = "Длительность съёмки должна быть не меньше 1 секунды") @field:DecimalMin(value = "1", message = "Длительность съёмки должна быть не меньше 1 секунды")
@field:Schema(description = "Длительность съёмки, секунды") @field:Schema(description = "Длительность съёмки, секунды")
@@ -1,11 +1,12 @@
package space.nstart.pcp.pcp_types_lib.dto.satellite.modes package space.nstart.pcp.pcp_types_lib.dto.satellite.modes
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
data class SurveyModeInfoDTO( data class SurveyModeInfoDTO(
val revolution : Long = 0, val revolution : Long = 0,
val time : LocalDateTime = LocalDateTime.now(), val time : LocalDateTime = UtcDateTimes.now(),
val timStop : LocalDateTime = LocalDateTime.now(), val timStop : LocalDateTime = UtcDateTimes.now(),
val duration : Double = 0.0, val duration : Double = 0.0,
val roll : Double = 0.0, val roll : Double = 0.0,
val contourWKT : String = "", val contourWKT : String = "",
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.fasterxml.jackson.databind.annotation.JsonSerialize
import space.nstart.pcp.pcp_types_lib.utils.json.LocalDateTimeTimeZoneSerializer import space.nstart.pcp.pcp_types_lib.utils.json.LocalDateTimeTimeZoneSerializer
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
@@ -22,7 +23,7 @@ class KafkaMessage<out T> (
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS[xxx]") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS[xxx]")
@JsonSerialize(using = LocalDateTimeTimeZoneSerializer::class) @JsonSerialize(using = LocalDateTimeTimeZoneSerializer::class)
val time: LocalDateTime = LocalDateTime.now() val time: LocalDateTime = UtcDateTimes.now()
val dataContentType = "application/json" val dataContentType = "application/json"
val specVersion = "1.0" val specVersion = "1.0"
@@ -0,0 +1,60 @@
package space.nstart.pcp.pcp_types_lib.utils
import java.time.Clock
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
/**
* Shared UTC helpers for services that still keep LocalDateTime in domain and DB models.
*
* LocalDateTime values are treated as UTC-naive: the stored object has no zone, but its
* numeric fields represent a UTC instant. Public API may receive either explicit UTC/offset
* ISO strings or legacy naive ISO strings; explicit offsets are normalized to UTC.
*/
object UtcDateTimes {
val UTC: ZoneOffset = ZoneOffset.UTC
fun now(clock: Clock = Clock.systemUTC()): LocalDateTime =
LocalDateTime.ofInstant(clock.instant(), UTC)
fun parse(value: String): LocalDateTime {
val normalized = normalize(value)
parseOffset(normalized)?.let { return it }
parseInstant(normalized)?.let { return it }
parseLocal(normalized)?.let { return it }
throw DateTimeParseException(
"Expected ISO-8601 UTC/offset date-time or legacy UTC-naive date-time",
value,
0
)
}
fun format(value: LocalDateTime): String = value.toInstant(UTC).toString()
fun parseOrNull(value: String): LocalDateTime? = runCatching { parse(value) }.getOrNull()
private fun normalize(value: String): String {
val trimmed = value.trim().replace(Regex(" (\\d{2}:\\d{2})$"), "+\$1")
return if (trimmed.length == 16) "$trimmed:00" else trimmed
}
private fun parseOffset(value: String): LocalDateTime? = runCatching {
OffsetDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME)
.withOffsetSameInstant(UTC)
.toLocalDateTime()
}.getOrNull()
private fun parseInstant(value: String): LocalDateTime? = runCatching {
LocalDateTime.ofInstant(Instant.parse(value), UTC)
}.getOrNull()
private fun parseLocal(value: String): LocalDateTime? = runCatching {
LocalDateTime.parse(value, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
}.getOrNull()
}
@@ -3,20 +3,16 @@ package space.nstart.pcp.pcp_types_lib.utils.json
import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.SerializerProvider
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
class LocalDateTimeTimeZoneSerializer: JsonSerializer<LocalDateTime?>() { class LocalDateTimeTimeZoneSerializer: JsonSerializer<LocalDateTime?>() {
companion object {
val fmt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSxxx")
}
override fun serialize(p0: LocalDateTime?, p1: JsonGenerator, p2: SerializerProvider) { override fun serialize(p0: LocalDateTime?, p1: JsonGenerator, p2: SerializerProvider) {
if (p0 == null) {
//todo: проверить корректность зоны по нормальным правилам p1.writeNull()
// p1.writeString(p0?.atZone(ZoneId.of("Europe/Moscow"))?.format(fmt)) } else {
p1.writeString(p0?.atZone(ZoneId.systemDefault())?.format(fmt)) p1.writeString(UtcDateTimes.format(p0))
}
} }
} }
@@ -0,0 +1,29 @@
package space.nstart.pcp.pcp_types_lib.utils
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.LocalDateTime
class UtcDateTimesTest {
@Test
fun `normalizes offset date-time to utc naive LocalDateTime`() {
val actual = UtcDateTimes.parse("2026-06-17T09:28:27.334+03:00")
assertEquals(LocalDateTime.of(2026, 6, 17, 6, 28, 27, 334_000_000), actual)
}
@Test
fun `treats legacy naive date-time as utc naive LocalDateTime`() {
val actual = UtcDateTimes.parse("2026-06-17T06:28:27.334")
assertEquals(LocalDateTime.of(2026, 6, 17, 6, 28, 27, 334_000_000), actual)
}
@Test
fun `formats utc naive LocalDateTime with Z suffix`() {
val actual = UtcDateTimes.format(LocalDateTime.of(2026, 6, 17, 6, 28, 27, 334_000_000))
assertEquals("2026-06-17T06:28:27.334Z", actual)
}
}
@@ -8,10 +8,12 @@ import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AngularMotionCalculationRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.AngularMotionCalculationRequestDTO
import java.net.URLDecoder import java.net.URLDecoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeParseException
@RestController @RestController
@RequestMapping("/api/angular-motion") @RequestMapping("/api/angular-motion")
@@ -43,11 +45,11 @@ class AngularMotionController(
return null return null
} }
val decoded = URLDecoder.decode(raw, StandardCharsets.UTF_8) val decoded = URLDecoder.decode(raw, StandardCharsets.UTF_8)
val normalized = if (decoded.length == 16) "$decoded:00" else decoded return try {
return runCatching { LocalDateTime.parse(normalized) } UtcDateTimes.parse(decoded)
.getOrElse { } catch (_: DateTimeParseException) {
throw CustomValidationException( throw CustomValidationException(
"Некорректный формат времени '$decoded'. Ожидается yyyy-MM-dd'T'HH:mm или yyyy-MM-dd'T'HH:mm:ss", "Некорректный формат времени '$decoded'. Ожидается ISO-8601 UTC/offset, например 2026-06-17T06:28:27Z, или legacy UTC-naive yyyy-MM-dd'T'HH:mm:ss",
) )
} }
} }
@@ -0,0 +1,12 @@
package space.nstart.pcp.pcp_request_service.configuration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Clock
@Configuration
class TimeConfiguration {
@Bean
fun clock(): Clock = Clock.systemUTC()
}
@@ -0,0 +1,40 @@
package space.nstart.pcp.pcp_request_service.configuration
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import space.nstart.pcp.pcp_request_service.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.LocalDateTime
@Configuration
class UtcTimeJsonConfiguration {
@Bean
fun ballisticsUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder ->
builder.addModule(
SimpleModule("pcp-ballistics-utc-local-date-time")
.addSerializer(LocalDateTime::class.java, UtcLocalDateTimeSerializer())
.addDeserializer(LocalDateTime::class.java, UtcLocalDateTimeDeserializer())
)
}
}
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)
}
}
@@ -15,9 +15,11 @@ import org.springframework.web.bind.annotation.RestController
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService
import space.nstart.pcp.pcp_request_service.service.SatelliteService import space.nstart.pcp.pcp_request_service.service.SatelliteService
import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeParseException
@RestController @RestController
@@ -61,11 +63,11 @@ class SatelliteController {
@Parameter(description = "Идентификатор NORAD", example = "56756") @Parameter(description = "Идентификатор NORAD", example = "56756")
@PathVariable("norad_id") noradId : Long, @PathVariable("norad_id") noradId : Long,
@Parameter(description = "Время начала (если null, то мин. время для выбранного КА)") @Parameter(description = "Время начала (если null, то мин. время для выбранного КА)")
@RequestParam("time_start") timeStart : LocalDateTime?, @RequestParam("time_start", required = false) timeStart : String?,
@Parameter(description = "Время конца (если null, то макс. время для выбранного КА)") @Parameter(description = "Время конца (если null, то макс. время для выбранного КА)")
@RequestParam("time_stop") timeStrop : LocalDateTime? @RequestParam("time_stop", required = false) timeStrop : String?
) = ) =
satelliteService.getOrbPoints(noradId, timeStart, timeStrop) satelliteService.getOrbPoints(noradId, parseOptionalUtcDateTime("time_start", timeStart), parseOptionalUtcDateTime("time_stop", timeStrop))
@Operation( @Operation(
summary = "Получить доступные интервалы прогноза по всем КА", summary = "Получить доступные интервалы прогноза по всем КА",
@@ -74,9 +76,9 @@ class SatelliteController {
@GetMapping("/orbit/availability") @GetMapping("/orbit/availability")
fun getOrbitAvailability( fun getOrbitAvailability(
@Parameter(description = "Проверяемое время. Если null, возвращаются все КА, для которых есть точки ПДЦМ") @Parameter(description = "Проверяемое время. Если null, возвращаются все КА, для которых есть точки ПДЦМ")
@RequestParam("time", required = false) time: LocalDateTime? @RequestParam("time", required = false) time: String?
) = ) =
satelliteService.getOrbitAvailability(time) satelliteService.getOrbitAvailability(parseOptionalUtcDateTime("time", time))
@GetMapping("/orbit/availability/{id}") @GetMapping("/orbit/availability/{id}")
@@ -96,11 +98,11 @@ class SatelliteController {
@Parameter(description = "Идентификатор NORAD", example = "56756") @Parameter(description = "Идентификатор NORAD", example = "56756")
@PathVariable("norad_id") noradId : Long, @PathVariable("norad_id") noradId : Long,
@Parameter(description = "Время начала (если null, то мин. время для выбранного КА)") @Parameter(description = "Время начала (если null, то мин. время для выбранного КА)")
@RequestParam("time_start") timeStart : LocalDateTime?, @RequestParam("time_start", required = false) timeStart : String?,
@Parameter(description = "Время конца (если null, то макс. время для выбранного КА)") @Parameter(description = "Время конца (если null, то макс. время для выбранного КА)")
@RequestParam("time_stop") timeStrop : LocalDateTime? @RequestParam("time_stop", required = false) timeStrop : String?
) = ) =
satelliteService.getAscNodes(noradId, timeStart, timeStrop) satelliteService.getAscNodes(noradId, parseOptionalUtcDateTime("time_start", timeStart), parseOptionalUtcDateTime("time_stop", timeStrop))
@Operation( @Operation(
@@ -112,11 +114,11 @@ class SatelliteController {
@Parameter(description = "Идентификатор NORAD", example = "56756") @Parameter(description = "Идентификатор NORAD", example = "56756")
@PathVariable("norad_id") noradId : Long, @PathVariable("norad_id") noradId : Long,
@Parameter(description = "Время начала (если null, то мин. время для выбранного КА)") @Parameter(description = "Время начала (если null, то мин. время для выбранного КА)")
@RequestParam("time_start") timeStart : LocalDateTime?, @RequestParam("time_start", required = false) timeStart : String?,
@Parameter(description = "Время конца (если null, то макс. время для выбранного КА)") @Parameter(description = "Время конца (если null, то макс. время для выбранного КА)")
@RequestParam("time_stop") timeStrop : LocalDateTime? @RequestParam("time_stop", required = false) timeStrop : String?
) = ) =
satelliteService.getFL(noradId, timeStart, timeStrop, 60.0) satelliteService.getFL(noradId, parseOptionalUtcDateTime("time_start", timeStart), parseOptionalUtcDateTime("time_stop", timeStrop), 60.0)
@@ -130,11 +132,11 @@ class SatelliteController {
@Parameter(description = "Идентификатор NORAD", example = "56756") @Parameter(description = "Идентификатор NORAD", example = "56756")
@PathVariable("norad_id") noradId : Long, @PathVariable("norad_id") noradId : Long,
@Parameter(description = "Время начала (если null, то мин. время для выбранного КА)") @Parameter(description = "Время начала (если null, то мин. время для выбранного КА)")
@RequestParam("time_start") timeStart : LocalDateTime, @RequestParam("time_start") timeStart : String,
@Parameter(description = "Время конца (если null, то макс. время для выбранного КА)") @Parameter(description = "Время конца (если null, то макс. время для выбранного КА)")
@RequestParam("time_stop") timeStrop : LocalDateTime @RequestParam("time_stop") timeStrop : String
) = ) =
satelliteService.getRVA(noradId, timeStart, timeStrop) satelliteService.getRVA(noradId, parseRequiredUtcDateTime("time_start", timeStart), parseRequiredUtcDateTime("time_stop", timeStrop))
@PostMapping("/{satellite_id}/clear") @PostMapping("/{satellite_id}/clear")
@@ -143,4 +145,16 @@ class SatelliteController {
) = ) =
satelliteService.clear(satelliteId) satelliteService.clear(satelliteId)
private fun parseOptionalUtcDateTime(name: String, value: String?): LocalDateTime? =
value?.let { parseRequiredUtcDateTime(name, it) }
private fun parseRequiredUtcDateTime(name: String, value: String): LocalDateTime =
try {
UtcDateTimes.parse(value)
} catch (_: DateTimeParseException) {
throw CustomValidationException(
"Некорректный формат параметра '$name': '$value'. Ожидается ISO-8601 UTC/offset, например 2026-06-17T06:28:27Z, или legacy UTC-naive yyyy-MM-dd'T'HH:mm:ss",
)
}
} }
@@ -1,5 +1,6 @@
package space.nstart.pcp.pcp_request_service.entity package space.nstart.pcp.pcp_request_service.entity
import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes
import ballistics.types.KeplerParams import ballistics.types.KeplerParams
import ballistics.types.RevolutionParameter import ballistics.types.RevolutionParameter
import ballistics.utils.fromDateTime import ballistics.utils.fromDateTime
@@ -26,7 +27,7 @@ class AscNodeEntity(
@Column(nullable = false) @Column(nullable = false)
val satelliteId : Long = 0, val satelliteId : Long = 0,
@Column(name = "t", nullable = false) @Column(name = "t", nullable = false)
val time : LocalDateTime = LocalDateTime.now(), val time : LocalDateTime = UtcDateTimes.now(),
@Column(nullable = false) @Column(nullable = false)
val revolution : Long = 0, val revolution : Long = 0,
val longitude : Double = 0.0, val longitude : Double = 0.0,
@@ -1,5 +1,6 @@
package space.nstart.pcp.pcp_request_service.entity package space.nstart.pcp.pcp_request_service.entity
import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes
import ballistics.utils.toDateTime import ballistics.utils.toDateTime
import jakarta.persistence.Column import jakarta.persistence.Column
import jakarta.persistence.Entity import jakarta.persistence.Entity
@@ -26,13 +27,13 @@ class EarthCellViewsEntity(
@Column(name = "rev_begin", nullable = false) @Column(name = "rev_begin", nullable = false)
val revolutionBegin : Long = 0, val revolutionBegin : Long = 0,
@Column(nullable = false) @Column(nullable = false)
val timeBegin : LocalDateTime = LocalDateTime.now(), val timeBegin : LocalDateTime = UtcDateTimes.now(),
@Column(nullable = false) @Column(nullable = false)
val revSignBegin : Int = 0, val revSignBegin : Int = 0,
@Column(name = "rev_end", nullable = false) @Column(name = "rev_end", nullable = false)
val revolutionEnd: Long = 0, val revolutionEnd: Long = 0,
@Column(nullable = false) @Column(nullable = false)
val timeEnd : LocalDateTime = LocalDateTime.now(), val timeEnd : LocalDateTime = UtcDateTimes.now(),
@Column(nullable = false) @Column(nullable = false)
val revSignEnd : Int = 0, val revSignEnd : Int = 0,
@Column(name = "roll_min") @Column(name = "roll_min")
@@ -1,5 +1,6 @@
package space.nstart.pcp.pcp_request_service.entity package space.nstart.pcp.pcp_request_service.entity
import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes
import ballistics.types.FleghtLineSector import ballistics.types.FleghtLineSector
import ballistics.utils.toDateTime import ballistics.utils.toDateTime
import jakarta.persistence.Column import jakarta.persistence.Column
@@ -22,9 +23,9 @@ class EarthCoverageEntity(
@Column(nullable = false) @Column(nullable = false)
val cellNumber : Long = 0, val cellNumber : Long = 0,
@Column(nullable = false) @Column(nullable = false)
val tStart : LocalDateTime = LocalDateTime.now(), val tStart : LocalDateTime = UtcDateTimes.now(),
@Column(nullable = false) @Column(nullable = false)
val tStop : LocalDateTime = LocalDateTime.now(), val tStop : LocalDateTime = UtcDateTimes.now(),
) { ) {
constructor(fl : FleghtLineSector, sector : Int, sat : Long) : this( constructor(fl : FleghtLineSector, sector : Int, sat : Long) : this(
null, null,
@@ -1,5 +1,6 @@
package space.nstart.pcp.pcp_request_service.entity package space.nstart.pcp.pcp_request_service.entity
import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes
import ballistics.types.FlightLine import ballistics.types.FlightLine
import ballistics.utils.toDateTime import ballistics.utils.toDateTime
import jakarta.persistence.* import jakarta.persistence.*
@@ -19,7 +20,7 @@ class FlightLineEntity(
@Column(nullable = false) @Column(nullable = false)
val satelliteId : Long = 0, val satelliteId : Long = 0,
@Column(name = "t", nullable = false) @Column(name = "t", nullable = false)
val time : LocalDateTime = LocalDateTime.now(), val time : LocalDateTime = UtcDateTimes.now(),
@Column(nullable = false) @Column(nullable = false)
val revolution : Long = 0, val revolution : Long = 0,
@Column(nullable = false) @Column(nullable = false)
@@ -1,5 +1,6 @@
package space.nstart.pcp.pcp_request_service.entity package space.nstart.pcp.pcp_request_service.entity
import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes
import ballistics.types.OrbitalPoint import ballistics.types.OrbitalPoint
import ballistics.utils.fromDateTime import ballistics.utils.fromDateTime
import ballistics.utils.math.Vector3D import ballistics.utils.math.Vector3D
@@ -18,7 +19,7 @@ class PDCMEntity (
@Column(nullable = false) @Column(nullable = false)
val satelliteId : Long = 0, val satelliteId : Long = 0,
@Column(name = "t", nullable = false) @Column(name = "t", nullable = false)
val time : LocalDateTime = LocalDateTime.now(), val time : LocalDateTime = UtcDateTimes.now(),
@Column(nullable = false) @Column(nullable = false)
val revolution : Long = 0, val revolution : Long = 0,
val x: Double = 0.0, val x: Double = 0.0,
@@ -1,5 +1,6 @@
package space.nstart.pcp.pcp_request_service.entity package space.nstart.pcp.pcp_request_service.entity
import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes
import jakarta.persistence.Column import jakarta.persistence.Column
import jakarta.persistence.Entity import jakarta.persistence.Entity
import jakarta.persistence.EnumType import jakarta.persistence.EnumType
@@ -27,7 +28,7 @@ class SatelliteInitialConditionEntity(
@Column(name = "movement_model", nullable = false) @Column(name = "movement_model", nullable = false)
val movementModel: MovementModel = MovementModel.FOTO, val movementModel: MovementModel = MovementModel.FOTO,
@Column(name = "orb_time", nullable = false) @Column(name = "orb_time", nullable = false)
val orbTime: LocalDateTime = LocalDateTime.now(), val orbTime: LocalDateTime = UtcDateTimes.now(),
@Column(name = "orb_revolution", nullable = false) @Column(name = "orb_revolution", nullable = false)
val orbRevolution: Long = 0, val orbRevolution: Long = 0,
@Column(nullable = false) @Column(nullable = false)
@@ -47,13 +48,13 @@ class SatelliteInitialConditionEntity(
@Column(nullable = false) @Column(nullable = false)
val f81: Double = 147.8, val f81: Double = 147.8,
@Column(name = "received_at", nullable = false) @Column(name = "received_at", nullable = false)
val receivedAt: LocalDateTime = LocalDateTime.now(), val receivedAt: LocalDateTime = UtcDateTimes.now(),
@Column(name = "calculated_at") @Column(name = "calculated_at")
var calculatedAt: LocalDateTime? = null, var calculatedAt: LocalDateTime? = null,
@Column(name = "last_calculated", nullable = false) @Column(name = "last_calculated", nullable = false)
var lastCalculated: Boolean = false var lastCalculated: Boolean = false
) { ) {
constructor(request: SatelliteICDTO) : this( constructor(request: SatelliteICDTO, receivedAt: LocalDateTime = UtcDateTimes.now()) : this(
satelliteId = request.satelliteId, satelliteId = request.satelliteId,
movementModel = request.movementModel, movementModel = request.movementModel,
orbTime = request.ic.orbPoint.time, orbTime = request.ic.orbPoint.time,
@@ -65,10 +66,11 @@ class SatelliteInitialConditionEntity(
vy = request.ic.orbPoint.vy, vy = request.ic.orbPoint.vy,
vz = request.ic.orbPoint.vz, vz = request.ic.orbPoint.vz,
sBall = request.ic.sBall, sBall = request.ic.sBall,
f81 = request.ic.f81 f81 = request.ic.f81,
receivedAt = receivedAt
) )
fun markLastCalculated(calculatedAt: LocalDateTime = LocalDateTime.now()) { fun markLastCalculated(calculatedAt: LocalDateTime = UtcDateTimes.now()) {
this.calculatedAt = calculatedAt this.calculatedAt = calculatedAt
this.lastCalculated = true this.lastCalculated = true
} }
@@ -1,6 +1,8 @@
package space.nstart.pcp.pcp_request_service.service package space.nstart.pcp.pcp_request_service.service
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes
import java.time.Clock
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import space.nstart.pcp.pcp_request_service.entity.SatelliteInitialConditionEntity import space.nstart.pcp.pcp_request_service.entity.SatelliteInitialConditionEntity
import space.nstart.pcp.pcp_request_service.repository.SatelliteInitialConditionRepository import space.nstart.pcp.pcp_request_service.repository.SatelliteInitialConditionRepository
@@ -8,17 +10,18 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
@Service @Service
class SatelliteInitialConditionService( class SatelliteInitialConditionService(
private val satelliteInitialConditionRepository: SatelliteInitialConditionRepository private val satelliteInitialConditionRepository: SatelliteInitialConditionRepository,
private val clock: Clock
) { ) {
@Transactional @Transactional
fun saveReceived(request: SatelliteICDTO): SatelliteInitialConditionEntity = fun saveReceived(request: SatelliteICDTO): SatelliteInitialConditionEntity =
satelliteInitialConditionRepository.save(SatelliteInitialConditionEntity(request)) satelliteInitialConditionRepository.save(SatelliteInitialConditionEntity(request, UtcDateTimes.now(clock)))
@Transactional @Transactional
fun markLastCalculated(initialCondition: SatelliteInitialConditionEntity) { fun markLastCalculated(initialCondition: SatelliteInitialConditionEntity) {
satelliteInitialConditionRepository.clearLastCalculated(initialCondition.satelliteId) satelliteInitialConditionRepository.clearLastCalculated(initialCondition.satelliteId)
initialCondition.markLastCalculated() initialCondition.markLastCalculated(UtcDateTimes.now(clock))
satelliteInitialConditionRepository.save(initialCondition) satelliteInitialConditionRepository.save(initialCondition)
} }
@@ -44,6 +44,7 @@ import space.nstart.pcp.pcp_request_service.repository.EarthCoverageRepository
import space.nstart.pcp.pcp_request_service.repository.FlightLineRepository import space.nstart.pcp.pcp_request_service.repository.FlightLineRepository
import space.nstart.pcp.pcp_request_service.repository.PDCMRepository import space.nstart.pcp.pcp_request_service.repository.PDCMRepository
import space.nstart.pcp.pcp_request_service.utils.ContourClipService import space.nstart.pcp.pcp_request_service.utils.ContourClipService
import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
@@ -62,6 +63,7 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityD
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
import tools.jackson.databind.ObjectMapper import tools.jackson.databind.ObjectMapper
import java.time.Clock
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.collections.forEach import kotlin.collections.forEach
import kotlin.math.PI import kotlin.math.PI
@@ -105,6 +107,8 @@ class SatelliteService {
@Autowired @Autowired
private lateinit var stationCatalogClient: StationCatalogClient private lateinit var stationCatalogClient: StationCatalogClient
@Autowired
private lateinit var clock: Clock
val astro = AstronomerJ2000(EarthType.PZ90d02) val astro = AstronomerJ2000(EarthType.PZ90d02)
@@ -761,8 +765,8 @@ class SatelliteService {
fun getOrbPoints(id : Long, tn : LocalDateTime?, tk : LocalDateTime?) = fun getOrbPoints(id : Long, tn : LocalDateTime?, tk : LocalDateTime?) =
pdcmRepository.findBySatelliteIdAndTimeBetween( pdcmRepository.findBySatelliteIdAndTimeBetween(
id, id,
tn?: LocalDateTime.now(), tn?: UtcDateTimes.now(clock),
tk?: LocalDateTime.now().plusDays(1)) tk?: UtcDateTimes.now(clock).plusDays(1))
.map { it.toDTO() .map { it.toDTO()
} }
@@ -780,8 +784,8 @@ class SatelliteService {
fun getFL(id : Long, tn : LocalDateTime?, tk : LocalDateTime?, step : Double?) = fun getFL(id : Long, tn : LocalDateTime?, tk : LocalDateTime?, step : Double?) =
flightLineRepository.findBySatelliteIdAndTimeBetween(id, flightLineRepository.findBySatelliteIdAndTimeBetween(id,
tn?: LocalDateTime.now(), tn?: UtcDateTimes.now(clock),
tk?: LocalDateTime.now().plusDays(1) tk?: UtcDateTimes.now(clock).plusDays(1)
) )
.map { it.toDTO() } .map { it.toDTO() }
@@ -902,8 +906,8 @@ class SatelliteService {
return ascNodeRepository.findBySatelliteIdAndTimeBetween( return ascNodeRepository.findBySatelliteIdAndTimeBetween(
id, id,
tn ?: LocalDateTime.now(), tn ?: UtcDateTimes.now(clock),
tk ?: LocalDateTime.now().plusDays(1) tk ?: UtcDateTimes.now(clock).plusDays(1)
) )
.map { .map {
val keps = calculateKeplerParams( val keps = calculateKeplerParams(
@@ -0,0 +1,59 @@
package space.nstart.pcp.pcp_request_service.utils
import java.time.Clock
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
/**
* UTC boundary helpers for pcp-ballistics-service.
*
* Domain and database still use LocalDateTime, but every value is treated as an
* absolute UTC instant without a stored zone (UTC-naive). Public API values may
* be sent either as UTC/offset ISO strings or as legacy naive ISO strings;
* offset values are normalized to UTC LocalDateTime.
*/
object UtcDateTimes {
val UTC: ZoneOffset = ZoneOffset.UTC
fun now(clock: Clock = Clock.systemUTC()): LocalDateTime =
LocalDateTime.ofInstant(clock.instant(), UTC)
fun parse(value: String): LocalDateTime {
val normalized = normalize(value)
parseOffset(normalized)?.let { return it }
parseInstant(normalized)?.let { return it }
parseLocal(normalized)?.let { return it }
throw DateTimeParseException(
"Expected ISO-8601 UTC/offset date-time or legacy UTC-naive date-time",
value,
0
)
}
fun format(value: LocalDateTime): String = value.toInstant(UTC).toString()
private fun normalize(value: String): String {
val trimmed = value.trim().replace(Regex(" (\\d{2}:\\d{2})$"), "+\$1")
return if (trimmed.length == 16) "$trimmed:00" else trimmed
}
private fun parseOffset(value: String): LocalDateTime? = runCatching {
OffsetDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME)
.withOffsetSameInstant(UTC)
.toLocalDateTime()
}.getOrNull()
private fun parseInstant(value: String): LocalDateTime? = runCatching {
LocalDateTime.ofInstant(Instant.parse(value), UTC)
}.getOrNull()
private fun parseLocal(value: String): LocalDateTime? = runCatching {
LocalDateTime.parse(value, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
}.getOrNull()
}
@@ -0,0 +1,40 @@
package space.nstart.pcp.pcp_request_service.utils
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.LocalDateTime
class UtcDateTimesTest {
@Test
fun `parses utc instant with z suffix`() {
assertEquals(
LocalDateTime.of(2026, 6, 17, 6, 28, 27, 334_000_000),
UtcDateTimes.parse("2026-06-17T06:28:27.334Z")
)
}
@Test
fun `normalizes offset date time to utc naive local date time`() {
assertEquals(
LocalDateTime.of(2026, 6, 17, 6, 28, 27),
UtcDateTimes.parse("2026-06-17T09:28:27+03:00")
)
}
@Test
fun `keeps legacy naive date time as utc naive`() {
assertEquals(
LocalDateTime.of(2026, 6, 17, 6, 28, 0),
UtcDateTimes.parse("2026-06-17T06:28")
)
}
@Test
fun `formats utc naive date time with z suffix`() {
assertEquals(
"2026-06-17T06:28:27.334Z",
UtcDateTimes.format(LocalDateTime.of(2026, 6, 17, 6, 28, 27, 334_000_000))
)
}
}
@@ -0,0 +1,51 @@
package space.nstart.pcp.pcp_satellites_service.configuration
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.config.WebFluxConfigurer
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.LocalDateTime
@Configuration
class UtcTimeJsonConfiguration : WebFluxConfigurer {
@Bean
fun complexMissionUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder ->
builder.addModule(
SimpleModule("pcp-complex-mission-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)
}
}
@@ -12,6 +12,7 @@ import jakarta.persistence.Id
import jakarta.persistence.Index import jakarta.persistence.Index
import jakarta.persistence.OneToMany import jakarta.persistence.OneToMany
import jakarta.persistence.Table import jakarta.persistence.Table
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
@Entity @Entity
@@ -34,7 +35,7 @@ class ComplexPlanRunEntity(
var status: ComplexPlanRunStatus = ComplexPlanRunStatus.CREATED, var status: ComplexPlanRunStatus = ComplexPlanRunStatus.CREATED,
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(), var createdAt: LocalDateTime = UtcDateTimes.now(),
@Column(name = "started_at") @Column(name = "started_at")
var startedAt: LocalDateTime? = null, var startedAt: LocalDateTime? = null,
@@ -46,10 +47,10 @@ class ComplexPlanRunEntity(
var durationMs: Long? = null, var durationMs: Long? = null,
@Column(name = "interval_start", nullable = false) @Column(name = "interval_start", nullable = false)
var intervalStart: LocalDateTime = LocalDateTime.now(), var intervalStart: LocalDateTime = UtcDateTimes.now(),
@Column(name = "interval_end", nullable = false) @Column(name = "interval_end", nullable = false)
var intervalEnd: LocalDateTime = LocalDateTime.now(), var intervalEnd: LocalDateTime = UtcDateTimes.now(),
@Column(name = "requested_by") @Column(name = "requested_by")
var requestedBy: String? = null, var requestedBy: String? = null,
@@ -17,6 +17,7 @@ import jakarta.persistence.PreUpdate
import jakarta.persistence.Table import jakarta.persistence.Table
import jakarta.persistence.Index import jakarta.persistence.Index
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.ModeDataRates import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.ModeDataRates
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
@Entity @Entity
@@ -46,10 +47,10 @@ class SatelliteModeEntity(
var type: SatelliteModeType = SatelliteModeType.SURVEY, var type: SatelliteModeType = SatelliteModeType.SURVEY,
@Column(name = "start_time", nullable = false) @Column(name = "start_time", nullable = false)
var startTime: LocalDateTime = LocalDateTime.now(), var startTime: LocalDateTime = UtcDateTimes.now(),
@Column(name = "end_time", nullable = false) @Column(name = "end_time", nullable = false)
var endTime: LocalDateTime = LocalDateTime.now(), var endTime: LocalDateTime = UtcDateTimes.now(),
@Column(name = "revolution", nullable = false) @Column(name = "revolution", nullable = false)
var revolution: Long = 0, var revolution: Long = 0,
@@ -12,6 +12,7 @@ import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne import jakarta.persistence.ManyToOne
import jakarta.persistence.OneToMany import jakarta.persistence.OneToMany
import jakarta.persistence.Table import jakarta.persistence.Table
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
@Entity @Entity
@@ -34,13 +35,13 @@ class SatelliteModeSnapshotEntity(
var satelliteId: Long = 0, var satelliteId: Long = 0,
@Column(name = "interval_start", nullable = false) @Column(name = "interval_start", nullable = false)
var intervalStart: LocalDateTime = LocalDateTime.now(), var intervalStart: LocalDateTime = UtcDateTimes.now(),
@Column(name = "interval_end", nullable = false) @Column(name = "interval_end", nullable = false)
var intervalEnd: LocalDateTime = LocalDateTime.now(), var intervalEnd: LocalDateTime = UtcDateTimes.now(),
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(), var createdAt: LocalDateTime = UtcDateTimes.now(),
@Column(name = "is_active", nullable = false) @Column(name = "is_active", nullable = false)
var isActive: Boolean = false, var isActive: Boolean = false,
@@ -2,12 +2,13 @@ package space.nstart.pcp.pcp_satellites_service.model.mode
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
import space.nstart.pcp.pcp_types_lib.dto.satellite.modes.SurveyModeInfoDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.modes.SurveyModeInfoDTO
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
class SurveyMode( class SurveyMode(
val revolution : Long = 1, val revolution : Long = 1,
val time : LocalDateTime = LocalDateTime.now(), val time : LocalDateTime = UtcDateTimes.now(),
val timeStop : LocalDateTime = LocalDateTime.now(), val timeStop : LocalDateTime = UtcDateTimes.now(),
val roll : Double = 0.0, val roll : Double = 0.0,
val latitude : Double = 0.0, val latitude : Double = 0.0,
val longitude : Double = 0.0, val longitude : Double = 0.0,
@@ -14,6 +14,7 @@ import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookingRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
import tools.jackson.databind.ObjectMapper import tools.jackson.databind.ObjectMapper
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
@Service @Service
@@ -34,8 +35,8 @@ class SlotService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
UriComponentsBuilder UriComponentsBuilder
.fromUriString("/api/slots/poly-cover") .fromUriString("/api/slots/poly-cover")
.queryParam("wkt", wkt) .queryParam("wkt", wkt)
.queryParam("timeStart", tn) .queryParam("timeStart", UtcDateTimes.format(tn))
.queryParam("timeStop", tk) .queryParam("timeStop", UtcDateTimes.format(tk))
.queryParam("cov", cov) .queryParam("cov", cov)
.apply { .apply {
sats.forEach { queryParam("satellites", it) } sats.forEach { queryParam("satellites", it) }
@@ -49,8 +50,8 @@ class SlotService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
.uri( .uri(
"/api/slots/poly-cover?wkt={wkt}&timeStart={timeStart}&timeStop={timeStop}&revSign={revSign}&cov={cov}", "/api/slots/poly-cover?wkt={wkt}&timeStart={timeStart}&timeStop={timeStop}&revSign={revSign}&cov={cov}",
wkt, wkt,
tn, UtcDateTimes.format(tn),
tk, UtcDateTimes.format(tk),
sign, sign,
cov cov
) )
@@ -71,8 +72,8 @@ class SlotService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
.uri( .uri(
"/api/slots/mission/satellite?satellite_id={satelliteId}&timeStart={timeStart}&timeStop={timeStop}", "/api/slots/mission/satellite?satellite_id={satelliteId}&timeStart={timeStart}&timeStop={timeStop}",
id, id,
tn, UtcDateTimes.format(tn),
tk UtcDateTimes.format(tk)
) )
.retrieve() .retrieve()
.onStatus({ status -> status.isError }) { response -> .onStatus({ status -> status.isError }) { response ->
@@ -16,6 +16,7 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjViewRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PointViewParamDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.PointViewParamDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -82,8 +83,8 @@ class BallisticsClient(
.uri { uriBuilder -> .uri { uriBuilder ->
uriBuilder uriBuilder
.path("/api/satellites/{satelliteId}/flight-line") .path("/api/satellites/{satelliteId}/flight-line")
.queryParam("time_start", timeStart) .queryParam("time_start", UtcDateTimes.format(timeStart))
.queryParam("time_stop", timeStop) .queryParam("time_stop", UtcDateTimes.format(timeStop))
.build(satelliteId) .build(satelliteId)
} }
.retrieve() .retrieve()
@@ -0,0 +1,55 @@
package space.nstart.pcp.pcp_coverage_scheme_service.configuration
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.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
@Configuration
class UtcTimeConfiguration : WebMvcConfigurer {
@Bean
fun clock(): Clock = Clock.systemUTC()
@Bean
fun coverageSchemeUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder ->
builder.addModule(
SimpleModule("pcp-coverage-scheme-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)
}
}
@@ -1,11 +1,21 @@
package space.nstart.pcp.complan package space.nstart.pcp.complan
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.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.registerKotlinModule import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime
@Configuration @Configuration
class JacksonConfig { class JacksonConfig {
@@ -13,8 +23,26 @@ class JacksonConfig {
fun objectMapper(): ObjectMapper { fun objectMapper(): ObjectMapper {
val mapper = ObjectMapper() val mapper = ObjectMapper()
mapper.registerModule(JavaTimeModule()) mapper.registerModule(JavaTimeModule())
.registerModule(utcLocalDateTimeModule())
.registerKotlinModule() .registerKotlinModule()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
return mapper return mapper
} }
} }
private fun utcLocalDateTimeModule(): SimpleModule = SimpleModule("pcp-dynamic-plan-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)
}
}
@@ -58,6 +58,7 @@ import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToLong import kotlin.math.roundToLong
import kotlin.math.sin import kotlin.math.sin
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
private const val INTERVAL_COVERAGE_AREA_TOLERANCE = 1.0e-9 private const val INTERVAL_COVERAGE_AREA_TOLERANCE = 1.0e-9
private const val INTERVAL_ROUTE_WIDTH_RATIO = 0.9 private const val INTERVAL_ROUTE_WIDTH_RATIO = 0.9
@@ -841,7 +842,7 @@ class BasicPlanCalculator(
val resultFilePath = if (request.saveResultToFile && status == ComplexPlanCalculationStatus.COMPLETED) { val resultFilePath = if (request.saveResultToFile && status == ComplexPlanCalculationStatus.COMPLETED) {
observationParametersDebugFileWriter.write( observationParametersDebugFileWriter.write(
ObservationParametersDebugFileDTO( ObservationParametersDebugFileDTO(
createdAt = LocalDateTime.now(), createdAt = UtcDateTimes.now(),
requestId = request.requestId, requestId = request.requestId,
satelliteIds = selectedSatelliteIds, satelliteIds = selectedSatelliteIds,
missingSatelliteIds = missingSatelliteIds, missingSatelliteIds = missingSatelliteIds,
@@ -0,0 +1,55 @@
package space.nstart.pcp.complan.config
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.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
@Configuration
class UtcTimeConfiguration : WebMvcConfigurer {
@Bean
fun clock(): Clock = Clock.systemUTC()
@Bean
fun dynamicPlanUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder ->
builder.addModule(
SimpleModule("pcp-dynamic-plan-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)
}
}
@@ -11,6 +11,7 @@ import jakarta.persistence.ManyToOne
import jakarta.persistence.Table import jakarta.persistence.Table
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
@Table(name = "dynamic_plan_route") @Table(name = "dynamic_plan_route")
@Entity @Entity
@@ -31,10 +32,10 @@ class DynamicPlanRouteEntity(
var satelliteId: Long = 0, var satelliteId: Long = 0,
@Column(name = "start_time", nullable = false) @Column(name = "start_time", nullable = false)
var startTime: LocalDateTime = LocalDateTime.now(), var startTime: LocalDateTime = UtcDateTimes.now(),
@Column(name = "end_time", nullable = false) @Column(name = "end_time", nullable = false)
var endTime: LocalDateTime = LocalDateTime.now(), var endTime: LocalDateTime = UtcDateTimes.now(),
@Column(name = "duration", nullable = false) @Column(name = "duration", nullable = false)
var duration: Double = 0.0, var duration: Double = 0.0,
@@ -11,6 +11,7 @@ import space.nstart.pcp.complan.types.DynamicPlanRevolutionMode
import space.nstart.pcp.complan.types.DynamicPlanRunStatus import space.nstart.pcp.complan.types.DynamicPlanRunStatus
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
@Table(name = "dynamic_plan_run") @Table(name = "dynamic_plan_run")
@Entity @Entity
@@ -47,7 +48,7 @@ class DynamicPlanRunEntity(
var filterCoveredRoutes: Boolean = false, var filterCoveredRoutes: Boolean = false,
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(), var createdAt: LocalDateTime = UtcDateTimes.now(),
@Column(name = "started_at") @Column(name = "started_at")
var startedAt: LocalDateTime? = null, var startedAt: LocalDateTime? = null,
@@ -10,10 +10,10 @@ import space.nstart.pcp.complan.types.ObjViewRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO as BallisticsObjDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO as BallisticsObjDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjViewRequestDTO as BallisticsObjViewRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
/**Получение данных от сервиса баллистики*/ /**Получение данных от сервиса баллистики*/
@@ -39,7 +39,7 @@ class BallTaskServce {
UriComponentsBuilder UriComponentsBuilder
.fromUriString(ballisticsServiceUrl) .fromUriString(ballisticsServiceUrl)
.path("/api/satellites/orbit/availability") .path("/api/satellites/orbit/availability")
.apply { time?.let { queryParam("time", it) } } .apply { time?.let { queryParam("time", UtcDateTimes.format(it)) } }
.toUriString() .toUriString()
) )
.retrieve() .retrieve()
@@ -53,8 +53,8 @@ class BallTaskServce {
UriComponentsBuilder UriComponentsBuilder
.fromUriString(ballisticsServiceUrl) .fromUriString(ballisticsServiceUrl)
.path("/api/satellites/$noradId/flight-line") .path("/api/satellites/$noradId/flight-line")
.queryParam("time_start", timeStart) .queryParam("time_start", UtcDateTimes.format(timeStart))
.queryParam("time_stop", timeStop) .queryParam("time_stop", UtcDateTimes.format(timeStop))
.toUriString() .toUriString()
) )
.retrieve() .retrieve()
@@ -65,7 +65,13 @@ class BallTaskServce {
RestClient.create() RestClient.create()
.post() .post()
.uri(buildBallisticsUri("/api/satellites/$noradId/extract-time")) .uri(buildBallisticsUri("/api/satellites/$noradId/extract-time"))
.body(request) .body(
mapOf(
"time" to UtcDateTimes.format(request.time),
"timeStop" to request.timeStop?.let { UtcDateTimes.format(it) },
"stepMs" to request.stepMs
)
)
.retrieve() .retrieve()
.body(ORB_POINT_LIST_TYPE) .body(ORB_POINT_LIST_TYPE)
.orEmpty() .orEmpty()
@@ -80,16 +86,16 @@ class BallTaskServce {
.post() .post()
.uri(buildBallisticsUri("/api/obj-view/mpl-square")) .uri(buildBallisticsUri("/api/obj-view/mpl-square"))
.body( .body(
BallisticsObjViewRequestDTO( mapOf(
satellites = satellites, "satellites" to satellites,
timeStart = timeStart, "timeStart" to UtcDateTimes.format(timeStart),
timeStop = timeStop, "timeStop" to UtcDateTimes.format(timeStop),
obj = BallisticsObjDTO( "obj" to BallisticsObjDTO(
id = obj.id.toString(), id = obj.id.toString(),
position = obj.position, position = obj.position,
contourWKT = obj.contourWKT contourWKT = obj.contourWKT
), ),
sunAngleMin = objects.sunAngleMin "sunAngleMin" to objects.sunAngleMin
) )
) )
.retrieve() .retrieve()
@@ -28,6 +28,7 @@ import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import kotlin.math.roundToLong import kotlin.math.roundToLong
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
@Service @Service
class DynamicPlanRunService( class DynamicPlanRunService(
@@ -59,7 +60,7 @@ class DynamicPlanRunService(
calculationMode = request.calculationMode, calculationMode = request.calculationMode,
revolutionMode = request.revolutionMode, revolutionMode = request.revolutionMode,
filterCoveredRoutes = request.filterCoveredRoutes, filterCoveredRoutes = request.filterCoveredRoutes,
createdAt = LocalDateTime.now() createdAt = UtcDateTimes.now()
) )
) )
} ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create calculation run") } ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create calculation run")
@@ -135,7 +136,7 @@ class DynamicPlanRunService(
return@execute 0 return@execute 0
} }
val finishedAt = LocalDateTime.now() val finishedAt = UtcDateTimes.now()
runs.forEach { run -> runs.forEach { run ->
run.status = DynamicPlanRunStatus.FAILED run.status = DynamicPlanRunStatus.FAILED
run.finishedAt = finishedAt run.finishedAt = finishedAt
@@ -171,7 +172,7 @@ class DynamicPlanRunService(
transactionTemplate.execute { transactionTemplate.execute {
val run = runRepository.findById(runId).orElse(null) ?: return@execute null val run = runRepository.findById(runId).orElse(null) ?: return@execute null
run.status = DynamicPlanRunStatus.RUNNING run.status = DynamicPlanRunStatus.RUNNING
run.startedAt = LocalDateTime.now() run.startedAt = UtcDateTimes.now()
run.errorMessage = null run.errorMessage = null
runRepository.save(run) runRepository.save(run)
run.toRequest() run.toRequest()
@@ -182,7 +183,7 @@ class DynamicPlanRunService(
val run = runRepository.findById(runId).orElseThrow() val run = runRepository.findById(runId).orElseThrow()
val result = output.result val result = output.result
run.status = DynamicPlanRunStatus.COMPLETED run.status = DynamicPlanRunStatus.COMPLETED
run.finishedAt = LocalDateTime.now() run.finishedAt = UtcDateTimes.now()
run.durationMs = result.durationMs run.durationMs = result.durationMs
run.resultStatus = result.status.name run.resultStatus = result.status.name
run.routesCount = result.routesCount run.routesCount = result.routesCount
@@ -200,7 +201,7 @@ class DynamicPlanRunService(
transactionTemplate.execute { transactionTemplate.execute {
val run = runRepository.findById(runId).orElseThrow() val run = runRepository.findById(runId).orElseThrow()
run.status = DynamicPlanRunStatus.FAILED run.status = DynamicPlanRunStatus.FAILED
run.finishedAt = LocalDateTime.now() run.finishedAt = UtcDateTimes.now()
run.errorMessage = error.message ?: error::class.java.simpleName run.errorMessage = error.message ?: error::class.java.simpleName
runRepository.save(run) runRepository.save(run)
} }
@@ -6,6 +6,7 @@ import org.locationtech.jts.geom.LineString
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import java.time.LocalDateTime import java.time.LocalDateTime
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Класс космического аппарата** * Класс космического аппарата**
@@ -17,9 +18,9 @@ class SpaceCraft (
val flightLine : MutableList<FlightLineDTO> = mutableListOf() val flightLine : MutableList<FlightLineDTO> = mutableListOf()
/**Время начала планирования аппарата*/ /**Время начала планирования аппарата*/
var timeStart: LocalDateTime = LocalDateTime.now() var timeStart: LocalDateTime = UtcDateTimes.now()
/**Время конца планирования аппарата*/ /**Время конца планирования аппарата*/
var timeStop : LocalDateTime = LocalDateTime.now() var timeStop : LocalDateTime = UtcDateTimes.now()
/** /**
* Создание линий полета восходящего и нисходящего витка * Создание линий полета восходящего и нисходящего витка
* @param geomFactory фабрика геометрии * @param geomFactory фабрика геометрии
@@ -0,0 +1,41 @@
package space.nstart.pcp.pcp_mission_planing_service.configuration
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.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
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 Jackson2UtcConfiguration {
@Bean
fun jackson2ObjectMapper(): ObjectMapper = jacksonObjectMapper()
.findAndRegisterModules()
.registerModule(utcLocalDateTimeModule())
}
private fun utcLocalDateTimeModule(): SimpleModule = SimpleModule("pcp-mission-planing-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)
}
}
@@ -0,0 +1,55 @@
package space.nstart.pcp.pcp_mission_planing_service.configuration
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.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
@Configuration
class UtcTimeConfiguration : WebMvcConfigurer {
@Bean
fun clock(): Clock = Clock.systemUTC()
@Bean
fun missionPlaningUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder ->
builder.addModule(
SimpleModule("pcp-mission-planing-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)
}
}
@@ -13,6 +13,7 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.PointViewParamDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamsWithPointsDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamsWithPointsDTO
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -55,8 +56,8 @@ class BallisticsService(webClientBuilderProvider: ObjectProvider<WebClient.Build
.get() .get()
.uri("/api/satellites/{id}/rva?time_start={time_start}&time_stop={time_stop}", .uri("/api/satellites/{id}/rva?time_start={time_start}&time_stop={time_stop}",
id, id,
tn, UtcDateTimes.format(tn),
tk) UtcDateTimes.format(tk))
.retrieve() .retrieve()
.bodyToFlux<RadioVisibilityAreaDTO>() .bodyToFlux<RadioVisibilityAreaDTO>()
.toIterable() .toIterable()
@@ -13,8 +13,9 @@ import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotsStatusChangedEvent import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotsStatusChangedEvent
import space.nstart.pcp.pcp_types_lib.message.KafkaMessage import space.nstart.pcp.pcp_types_lib.message.KafkaMessage
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.time.LocalDateTime import java.time.Clock
import java.util.UUID import java.util.UUID
@Component @Component
@@ -26,7 +27,8 @@ class BookedSlotsEventPublisher(
kafkaTemplateProvider: ObjectProvider<KafkaTemplate<String, Any>>, kafkaTemplateProvider: ObjectProvider<KafkaTemplate<String, Any>>,
objectMapperProvider: ObjectProvider<ObjectMapper>, objectMapperProvider: ObjectProvider<ObjectMapper>,
@Value("\${app.kafka.topics.booked-slots:pcp.booked-slots}") private val topic: String, @Value("\${app.kafka.topics.booked-slots:pcp.booked-slots}") private val topic: String,
@Value("\${spring.application.name:pcp-mission-planing-service}") private val applicationName: String @Value("\${spring.application.name:pcp-mission-planing-service}") private val applicationName: String,
private val clock: Clock = Clock.systemUTC()
) { ) {
private val logger = LoggerFactory.getLogger(this::class.java) private val logger = LoggerFactory.getLogger(this::class.java)
@@ -59,7 +61,7 @@ class BookedSlotsEventPublisher(
val event = BookedSlotsStatusChangedEvent( val event = BookedSlotsStatusChangedEvent(
bookedSlotIds = normalizedIds, bookedSlotIds = normalizedIds,
status = status, status = status,
occurredAt = LocalDateTime.now(), occurredAt = UtcDateTimes.now(clock),
sourceService = applicationName, sourceService = applicationName,
missionId = missionId, missionId = missionId,
modeId = modeId, modeId = modeId,
@@ -10,6 +10,7 @@ import reactor.core.publisher.Mono
import space.nstart.pcp.pcp_types_lib.configuration.CustomErrorException import space.nstart.pcp.pcp_types_lib.configuration.CustomErrorException
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeSnapshotResponseDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeSnapshotResponseDTO
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import tools.jackson.databind.ObjectMapper import tools.jackson.databind.ObjectMapper
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -70,8 +71,8 @@ class ComplexMissionService(webClientBuilderProvider: ObjectProvider<WebClient.B
.uri( .uri(
"/api/satellites/snapshots/{snapshotId}/modes?intervalStart={intervalStart}&intervalEnd={intervalEnd}", "/api/satellites/snapshots/{snapshotId}/modes?intervalStart={intervalStart}&intervalEnd={intervalEnd}",
snapshotId, snapshotId,
intervalStart, UtcDateTimes.format(intervalStart),
intervalEnd UtcDateTimes.format(intervalEnd)
) )
.retrieve() .retrieve()
.onStatus({ status -> status.isError }) { response -> .onStatus({ status -> status.isError }) { response ->
@@ -60,6 +60,8 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjViewRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.Clock
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
@@ -81,6 +83,7 @@ class MissionPlaningService(
private val complexMissionService: ComplexMissionService, private val complexMissionService: ComplexMissionService,
private val satelliteCatalogClient: SatelliteCatalogClient, private val satelliteCatalogClient: SatelliteCatalogClient,
private val satelliteMissionTypeFactory: SatelliteMissionTypeFactory, private val satelliteMissionTypeFactory: SatelliteMissionTypeFactory,
private val clock: Clock = Clock.systemUTC(),
private val logger: Logger = LoggerFactory.getLogger(MissionPlaningService::class.java) private val logger: Logger = LoggerFactory.getLogger(MissionPlaningService::class.java)
) { ) {
@@ -432,7 +435,7 @@ class MissionPlaningService(
overrideIntervalEnd: LocalDateTime? = null, overrideIntervalEnd: LocalDateTime? = null,
comPlanSnapshotId: Long? = null comPlanSnapshotId: Long? = null
): MissionSurveyCalculationResponseDTO { ): MissionSurveyCalculationResponseDTO {
val startedAt = LocalDateTime.now() val startedAt = UtcDateTimes.now(clock)
logger.info( logger.info(
"Расчет плана съемки stage 5 по persisted snapshot этапа 4 для миссии {}: requestedComPlanSnapshotId={}", "Расчет плана съемки stage 5 по persisted snapshot этапа 4 для миссии {}: requestedComPlanSnapshotId={}",
missionId, missionId,
@@ -520,7 +523,7 @@ class MissionPlaningService(
) )
if (survs.isEmpty()) { if (survs.isEmpty()) {
val finishedAt = LocalDateTime.now() val finishedAt = UtcDateTimes.now(clock)
return MissionSurveyCalculationResponseDTO( return MissionSurveyCalculationResponseDTO(
missionId = missionId, missionId = missionId,
completed = true, completed = true,
@@ -565,7 +568,7 @@ class MissionPlaningService(
modeBookings.size modeBookings.size
) )
val finishedAt = LocalDateTime.now() val finishedAt = UtcDateTimes.now(clock)
return MissionSurveyCalculationResponseDTO( return MissionSurveyCalculationResponseDTO(
missionId = missionId, missionId = missionId,
completed = true, completed = true,
@@ -1,10 +1,19 @@
package org.nstart.dep265.requestservice.config package org.nstart.dep265.requestservice.config
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.ObjectMapper
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime
@Configuration @Configuration
class JacksonConfig { class JacksonConfig {
@@ -18,5 +27,23 @@ class JacksonConfig {
return jacksonObjectMapper() return jacksonObjectMapper()
.registerModule(JavaTimeModule()) .registerModule(JavaTimeModule())
.findAndRegisterModules() .findAndRegisterModules()
.registerModule(utcLocalDateTimeModule())
}
private fun utcLocalDateTimeModule(): SimpleModule = SimpleModule("pcp-request-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)
} }
} }
@@ -0,0 +1,55 @@
package org.nstart.dep265.requestservice.config
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.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
@Configuration
class UtcTimeConfiguration : WebMvcConfigurer {
@Bean
fun clock(): Clock = Clock.systemUTC()
@Bean
fun requestServiceUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder ->
builder.addModule(
SimpleModule("pcp-request-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)
}
}
@@ -12,7 +12,6 @@ import org.nstart.dep265.requestservice.dto.RequestStatusDto
import org.nstart.dep265.requestservice.dto.RequestWithCellsResponseDto import org.nstart.dep265.requestservice.dto.RequestWithCellsResponseDto
import org.nstart.dep265.requestservice.dto.SurveyTypeDto import org.nstart.dep265.requestservice.dto.SurveyTypeDto
import org.nstart.dep265.requestservice.service.RequestService import org.nstart.dep265.requestservice.service.RequestService
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
@@ -41,10 +40,10 @@ class RequestController(
@RequestParam(required = false) surveyType: SurveyTypeDto?, @RequestParam(required = false) surveyType: SurveyTypeDto?,
@RequestParam(required = false) kpp: Int?, @RequestParam(required = false) kpp: Int?,
@RequestParam(required = false) highPriorityTransmit: Boolean?, @RequestParam(required = false) highPriorityTransmit: Boolean?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginFrom: LocalDateTime?, @RequestParam(required = false) beginFrom: LocalDateTime?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginTo: LocalDateTime?, @RequestParam(required = false) beginTo: LocalDateTime?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endFrom: LocalDateTime?, @RequestParam(required = false) endFrom: LocalDateTime?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endTo: LocalDateTime?, @RequestParam(required = false) endTo: LocalDateTime?,
@RequestParam(defaultValue = "false") includeDeleted: Boolean, @RequestParam(defaultValue = "false") includeDeleted: Boolean,
@RequestParam(defaultValue = "0") page: Int, @RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "50") size: Int, @RequestParam(defaultValue = "50") size: Int,
@@ -73,10 +72,10 @@ class RequestController(
@RequestParam(required = false) surveyType: SurveyTypeDto?, @RequestParam(required = false) surveyType: SurveyTypeDto?,
@RequestParam(required = false) kpp: Int?, @RequestParam(required = false) kpp: Int?,
@RequestParam(required = false) highPriorityTransmit: Boolean?, @RequestParam(required = false) highPriorityTransmit: Boolean?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginFrom: LocalDateTime?, @RequestParam(required = false) beginFrom: LocalDateTime?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginTo: LocalDateTime?, @RequestParam(required = false) beginTo: LocalDateTime?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endFrom: LocalDateTime?, @RequestParam(required = false) endFrom: LocalDateTime?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endTo: LocalDateTime?, @RequestParam(required = false) endTo: LocalDateTime?,
@RequestParam(defaultValue = "false") includeDeleted: Boolean, @RequestParam(defaultValue = "false") includeDeleted: Boolean,
@RequestParam(defaultValue = "0") page: Int, @RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "50") size: Int, @RequestParam(defaultValue = "50") size: Int,
@@ -7,6 +7,8 @@ import org.nstart.dep265.requestservice.repository.RequestCellRepository
import org.nstart.dep265.requestservice.repository.RequestRepository import org.nstart.dep265.requestservice.repository.RequestRepository
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.Clock
import java.time.LocalDateTime import java.time.LocalDateTime
class GridRebuildAlreadyRunningException : RuntimeException("Grid rebuild is already running") class GridRebuildAlreadyRunningException : RuntimeException("Grid rebuild is already running")
@@ -20,6 +22,7 @@ class GridRebuildService(
private val requestGridProjectionService: RequestGridProjectionService, private val requestGridProjectionService: RequestGridProjectionService,
private val earthCellRepository: EarthCellRepository, private val earthCellRepository: EarthCellRepository,
private val gridRebuildLockService: GridRebuildLockService, private val gridRebuildLockService: GridRebuildLockService,
private val clock: Clock = Clock.systemUTC(),
) { ) {
@Transactional @Transactional
fun rebuildGrid(): GridRebuildResponseDto { fun rebuildGrid(): GridRebuildResponseDto {
@@ -40,7 +43,7 @@ class GridRebuildService(
return GridRebuildResponseDto( return GridRebuildResponseDto(
cellsCount = earthCellRepository.count(), cellsCount = earthCellRepository.count(),
rebuiltRequestProjectionsCount = activeRequests.size.toLong(), rebuiltRequestProjectionsCount = activeRequests.size.toLong(),
rebuiltAt = LocalDateTime.now(), rebuiltAt = UtcDateTimes.now(clock),
) )
} }
@@ -11,6 +11,8 @@ import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionCallback import org.springframework.transaction.support.TransactionCallback
import org.springframework.transaction.support.TransactionOperations import org.springframework.transaction.support.TransactionOperations
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.Clock
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
@@ -25,6 +27,7 @@ class OutboxPublisherService(
@param:Qualifier("outboxTransactionOperations") @param:Qualifier("outboxTransactionOperations")
private val transactionOperations: TransactionOperations, private val transactionOperations: TransactionOperations,
private val clock: Clock = Clock.systemUTC(),
) { ) {
private val log = LoggerFactory.getLogger(this::class.java) private val log = LoggerFactory.getLogger(this::class.java)
@@ -42,7 +45,7 @@ class OutboxPublisherService(
return try { return try {
transactionOperations.execute( transactionOperations.execute(
TransactionCallback { TransactionCallback {
val now = LocalDateTime.now() val now = UtcDateTimes.now(clock)
val events = outboxEventRepository.findPublishableForUpdateSkipLocked( val events = outboxEventRepository.findPublishableForUpdateSkipLocked(
eventType = RequestStatusOutboxService.REQUEST_STATUS_EVENT_TYPE, eventType = RequestStatusOutboxService.REQUEST_STATUS_EVENT_TYPE,
now = now, now = now,
@@ -9,6 +9,8 @@ import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionCallback import org.springframework.transaction.support.TransactionCallback
import org.springframework.transaction.support.TransactionOperations import org.springframework.transaction.support.TransactionOperations
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.Clock
import java.time.LocalDateTime import java.time.LocalDateTime
/** /**
@@ -33,6 +35,7 @@ class RequestExpiryService(
@param:Qualifier("requestExpiryTransactionOperations") @param:Qualifier("requestExpiryTransactionOperations")
private val transactionOperations: TransactionOperations, private val transactionOperations: TransactionOperations,
private val clock: Clock = Clock.systemUTC(),
) { ) {
private val log = LoggerFactory.getLogger(this::class.java) private val log = LoggerFactory.getLogger(this::class.java)
@@ -55,7 +58,7 @@ class RequestExpiryService(
return 0 return 0
} }
val maxBatches = requestExpiryProperties.maxBatchesPerRun.coerceAtLeast(1) val maxBatches = requestExpiryProperties.maxBatchesPerRun.coerceAtLeast(1)
val now = LocalDateTime.now() val now = UtcDateTimes.now(clock)
var expiredTotal = 0 var expiredTotal = 0
var batchIndex = 0 var batchIndex = 0
@@ -38,6 +38,8 @@ import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort import org.springframework.data.domain.Sort
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.Clock
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
@@ -53,6 +55,7 @@ class RequestService(
private val requestGridProjectionService: RequestGridProjectionService, private val requestGridProjectionService: RequestGridProjectionService,
private val requestLifecycleService: RequestLifecycleService, private val requestLifecycleService: RequestLifecycleService,
private val entityManager: EntityManager, private val entityManager: EntityManager,
private val clock: Clock = Clock.systemUTC(),
) { ) {
@Transactional @Transactional
fun createRequest(request: CreateRequestDto): CreateRequestResponseDto { fun createRequest(request: CreateRequestDto): CreateRequestResponseDto {
@@ -65,7 +68,7 @@ class RequestService(
"beginDateTime должен быть раньше endDateTime" "beginDateTime должен быть раньше endDateTime"
} }
val now = LocalDateTime.now() val now = UtcDateTimes.now(clock)
val normalizedGeometry = geometryService.parsePolygonalGeometry(request.geometry) val normalizedGeometry = geometryService.parsePolygonalGeometry(request.geometry)
val normalizedGeometryWkt = geometryService.toWkt(normalizedGeometry) val normalizedGeometryWkt = geometryService.toWkt(normalizedGeometry)
val geometryArea = geometryService.area(normalizedGeometry) val geometryArea = geometryService.area(normalizedGeometry)
@@ -159,7 +162,7 @@ class RequestService(
@Transactional @Transactional
fun deleteRequest(id: UUID): DeleteRequestResponseDto? { fun deleteRequest(id: UUID): DeleteRequestResponseDto? {
val request = requestRepository.findById(id).orElse(null) ?: return null val request = requestRepository.findById(id).orElse(null) ?: return null
val deletedAt = request.deletedAt ?: LocalDateTime.now() val deletedAt = request.deletedAt ?: UtcDateTimes.now(clock)
request.status = RequestStatus.DELETED request.status = RequestStatus.DELETED
request.deletedAt = deletedAt request.deletedAt = deletedAt
request.updatedAt = deletedAt request.updatedAt = deletedAt
@@ -14,6 +14,8 @@ import org.springframework.dao.DuplicateKeyException
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionCallback import org.springframework.transaction.support.TransactionCallback
import org.springframework.transaction.support.TransactionOperations import org.springframework.transaction.support.TransactionOperations
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.Clock
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import kotlin.math.max import kotlin.math.max
@@ -35,6 +37,7 @@ class RouteMatchingService(
@param:Qualifier("routeMatchingTransactionOperations") @param:Qualifier("routeMatchingTransactionOperations")
private val routeMatchingTransactionOperations: TransactionOperations, private val routeMatchingTransactionOperations: TransactionOperations,
private val clock: Clock = Clock.systemUTC(),
) { ) {
private val log = LoggerFactory.getLogger(this::class.java) private val log = LoggerFactory.getLogger(this::class.java)
@@ -43,7 +46,7 @@ class RouteMatchingService(
val routeId = route.routeId val routeId = route.routeId
val routeRollRange = route.rollAngle val routeRollRange = route.rollAngle
val matchedAt: LocalDateTime = LocalDateTime.now() val matchedAt: LocalDateTime = UtcDateTimes.now(clock)
val routeGeometry = geometryService.parsePolygonalGeometry(route.geometry) val routeGeometry = geometryService.parsePolygonalGeometry(route.geometry)
val candidateRequestIds = requestRepository.findRequestIdsForRoute( val candidateRequestIds = requestRepository.findRequestIdsForRoute(
@@ -0,0 +1,55 @@
package space.nstart.pcp.routepassportconsumer.config
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.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
@Configuration
class UtcTimeConfiguration : WebMvcConfigurer {
@Bean
fun clock(): Clock = Clock.systemUTC()
@Bean
fun routeProcessingUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder ->
builder.addModule(
SimpleModule("pcp-route-processing-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)
}
}
@@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.ObjectMapper as JpaJsonObjectMapper
import tools.jackson.databind.ObjectMapper import tools.jackson.databind.ObjectMapper
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Сохраняет разобранный route passport и outbox-события в одной DB transaction. * Сохраняет разобранный route passport и outbox-события в одной DB transaction.
@@ -38,7 +39,7 @@ class RoutePassportIngestionService(
rawPayload: String, rawPayload: String,
sourceRecord: RoutePassportSourceRecordMetadata, sourceRecord: RoutePassportSourceRecordMetadata,
) { ) {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
val routePassportDto = parsedMessage.routePassportDto val routePassportDto = parsedMessage.routePassportDto
val existingRoutePassport = routePassportRepository.findById(routePassportDto.routeId) val existingRoutePassport = routePassportRepository.findById(routePassportDto.routeId)
val routeCreated = existingRoutePassport.isEmpty val routeCreated = existingRoutePassport.isEmpty
@@ -8,11 +8,11 @@ import space.nstart.pcp.pcp_types_lib.dto.routes.RawPassportEnvelopeDto
import space.nstart.pcp.pcp_types_lib.dto.routes.RoutePassportDto import space.nstart.pcp.pcp_types_lib.dto.routes.RoutePassportDto
import space.nstart.pcp.pcp_types_lib.dto.routes.RoutePassportMessageMetadata import space.nstart.pcp.pcp_types_lib.dto.routes.RoutePassportMessageMetadata
import space.nstart.pcp.routepassportconsumer.exception.RoutePassportContractException import space.nstart.pcp.routepassportconsumer.exception.RoutePassportContractException
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import tools.jackson.core.JacksonException import tools.jackson.core.JacksonException
import tools.jackson.databind.ObjectMapper import tools.jackson.databind.ObjectMapper
import java.math.BigDecimal import java.math.BigDecimal
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
/** /**
@@ -63,8 +63,8 @@ class RoutePassportJsonParserService(
routeNameShort = collection, routeNameShort = collection,
orbitNumber = properties.orbitNumber.toLong(), orbitNumber = properties.orbitNumber.toLong(),
orbitState = properties.orbitState, orbitState = properties.orbitState,
intervalBegin = LocalDateTime.parse(properties.intervalBegin), intervalBegin = UtcDateTimes.parse(properties.intervalBegin),
intervalEnd = LocalDateTime.parse(properties.intervalEnd), intervalEnd = UtcDateTimes.parse(properties.intervalEnd),
rollAngle = properties.rollAngle.toAngleRangeDto(fieldName = "rollAngle"), rollAngle = properties.rollAngle.toAngleRangeDto(fieldName = "rollAngle"),
visirAngle = properties.visirAngle.toAngleRangeDto(fieldName = "visirAngle"), visirAngle = properties.visirAngle.toAngleRangeDto(fieldName = "visirAngle"),
resolutionRange = properties.resolutionRange.toBigDecimal(), resolutionRange = properties.resolutionRange.toBigDecimal(),
@@ -10,6 +10,7 @@ import space.nstart.pcp.routepassportconsumer.repository.RoutePassportOutboxRepo
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Owns transactional claim and status transitions for route passport outbox rows. * Owns transactional claim and status transitions for route passport outbox rows.
@@ -32,7 +33,7 @@ class RoutePassportOutboxService(
return emptyList() return emptyList()
} }
val now = LocalDateTime.now() val now = UtcDateTimes.now()
val lockedUntil = now.plus(Duration.ofMillis(outboxProperties.lockTimeoutMs)) val lockedUntil = now.plus(Duration.ofMillis(outboxProperties.lockTimeoutMs))
val claimedRows = routePassportOutboxRepository.findDueForUpdateSkipLocked(outboxProperties.batchSize) val claimedRows = routePassportOutboxRepository.findDueForUpdateSkipLocked(outboxProperties.batchSize)
@@ -70,7 +71,7 @@ class RoutePassportOutboxService(
*/ */
@Transactional @Transactional
fun markPublished(outboxId: UUID) { fun markPublished(outboxId: UUID) {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
val outbox = findOutbox(outboxId) val outbox = findOutbox(outboxId)
outbox.status = RoutePassportOutboxStatus.PUBLISHED outbox.status = RoutePassportOutboxStatus.PUBLISHED
@@ -99,7 +100,7 @@ class RoutePassportOutboxService(
*/ */
@Transactional @Transactional
fun markRetryOrFailed(outboxId: UUID, exception: Exception) { fun markRetryOrFailed(outboxId: UUID, exception: Exception) {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
val outbox = findOutbox(outboxId) val outbox = findOutbox(outboxId)
outbox.attempts += 1 outbox.attempts += 1
@@ -0,0 +1,55 @@
package space.nstart.pcp.pcp_satellite_catalog_service.configuration
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.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
@Configuration
class UtcTimeConfiguration : WebMvcConfigurer {
@Bean
fun clock(): Clock = Clock.systemUTC()
@Bean
fun satelliteCatalogUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder ->
builder.addModule(
SimpleModule("pcp-satellite-catalog-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)
}
}
@@ -1,15 +1,24 @@
package space.nstart.pcp_tgu_service.config package space.nstart.pcp_tgu_service.config
/** Настраивает Jackson-сериализацию для payload'ов клиента Camunda. */ /** Настраивает 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.ObjectMapper
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper 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.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.KotlinModule
import io.camunda.client.api.JsonMapper as CamundaJsonMapper import io.camunda.client.api.JsonMapper as CamundaJsonMapper
import io.camunda.client.impl.CamundaObjectMapper import io.camunda.client.impl.CamundaObjectMapper
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime
@Configuration @Configuration
class CamundaJacksonConfig { class CamundaJacksonConfig {
@@ -24,6 +33,7 @@ class CamundaJacksonConfig {
return JsonMapper.builder() return JsonMapper.builder()
.addModule(KotlinModule.Builder().build()) .addModule(KotlinModule.Builder().build())
.addModule(JavaTimeModule()) .addModule(JavaTimeModule())
.addModule(jackson2UtcLocalDateTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build() .build()
} }
@@ -34,3 +44,21 @@ class CamundaJacksonConfig {
return CamundaObjectMapper(objectMapper) 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)
}
}
@@ -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)
}
}
@@ -1,7 +1,6 @@
package space.nstart.pcp_tgu_service.controller package space.nstart.pcp_tgu_service.controller
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty 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.GetMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
@@ -90,12 +89,8 @@ class TestTguController(
@GetMapping("/external-points/{noradId}") @GetMapping("/external-points/{noradId}")
fun getExternalPoints( fun getExternalPoints(
@PathVariable noradId: Long, @PathVariable noradId: Long,
@RequestParam("time_start") @RequestParam("time_start") timeStart: LocalDateTime,
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @RequestParam("time_stop") timeStop: LocalDateTime
timeStart: LocalDateTime,
@RequestParam("time_stop")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
timeStop: LocalDateTime
): String = ): String =
externalPointsClient.fetchRawVisibilityPayload(noradId, timeStart, timeStop) externalPointsClient.fetchRawVisibilityPayload(noradId, timeStart, timeStop)
@@ -10,8 +10,8 @@ import java.util.UUID
/** /**
* DTO ответа REST API для плана. * DTO ответа REST API для плана.
* *
* Времена ([startTime]/[endTime]) naive `LocalDateTime`, сериализуются как ISO-строка без зоны * Времена ([startTime]/[endTime]) UTC-naive `LocalDateTime` в домене/БД.
* (напр. `"2026-05-29T01:00:00"`). Хранение, домен и API работают на одних настенных часах. * На REST-границе сериализуются как ISO-8601 UTC с `Z` (например `"2026-05-29T01:00:00Z"`).
*/ */
data class PlanResponse( data class PlanResponse(
@@ -21,10 +21,10 @@ data class PlanResponse(
/** Идентификатор космического аппарата */ /** Идентификатор космического аппарата */
val spacecraftId: String, val spacecraftId: String,
/** Время начала плана (ISO без зоны). */ /** Время начала плана (UTC). */
val startTime: LocalDateTime, val startTime: LocalDateTime,
/** Время окончания плана (ISO без зоны). */ /** Время окончания плана (UTC). */
val endTime: LocalDateTime, val endTime: LocalDateTime,
/** Идентификатор пункта закладки */ /** Идентификатор пункта закладки */
@@ -8,6 +8,7 @@ import jakarta.persistence.Id
import jakarta.persistence.Table import jakarta.persistence.Table
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Persisted job на запуск короткого BPMN-процесса обновления слотов accepted plan. * Persisted job на запуск короткого BPMN-процесса обновления слотов accepted plan.
@@ -39,10 +40,10 @@ class AcceptedPlanSlotsUpdateJobEntity(
var lastError: String? = null, var lastError: String? = null,
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(), var createdAt: LocalDateTime = UtcDateTimes.now(),
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now() var updatedAt: LocalDateTime = UtcDateTimes.now()
) { ) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */ /** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(planId = UUID.randomUUID()) constructor() : this(planId = UUID.randomUUID())
@@ -8,6 +8,7 @@ import jakarta.persistence.Id
import jakarta.persistence.Table import jakarta.persistence.Table
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Одна попытка выдачи persisted plan через один BPMN process instance. * Одна попытка выдачи persisted plan через один BPMN process instance.
@@ -45,10 +46,10 @@ class PlanIssueAttemptEntity(
var finishedAt: LocalDateTime? = null, var finishedAt: LocalDateTime? = null,
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(), var createdAt: LocalDateTime = UtcDateTimes.now(),
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now() var updatedAt: LocalDateTime = UtcDateTimes.now()
) { ) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */ /** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(attemptId = UUID.randomUUID(), planId = UUID.randomUUID(), attemptNo = 0) constructor() : this(attemptId = UUID.randomUUID(), planId = UUID.randomUUID(), attemptNo = 0)
@@ -8,6 +8,7 @@ import jakarta.persistence.Id
import jakarta.persistence.Table import jakarta.persistence.Table
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Persisted business-plan с устойчивым planId по unique business key. * Persisted business-plan с устойчивым planId по unique business key.
@@ -56,10 +57,10 @@ class PlannedPlanEntity(
var dismissedAt: LocalDateTime? = null, var dismissedAt: LocalDateTime? = null,
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(), var createdAt: LocalDateTime = UtcDateTimes.now(),
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now() var updatedAt: LocalDateTime = UtcDateTimes.now()
) { ) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */ /** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this( constructor() : this(
@@ -6,6 +6,7 @@ import jakarta.persistence.Id
import jakarta.persistence.Table import jakarta.persistence.Table
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Dedup-запись Kafka decision event по eventId. * Dedup-запись Kafka decision event по eventId.
@@ -18,7 +19,7 @@ class ProcessedPlanDecisionEventEntity(
var eventId: UUID, var eventId: UUID,
@Column(name = "received_at", nullable = false) @Column(name = "received_at", nullable = false)
var receivedAt: LocalDateTime = LocalDateTime.now() var receivedAt: LocalDateTime = UtcDateTimes.now()
) { ) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */ /** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(eventId = UUID.randomUUID()) constructor() : this(eventId = UUID.randomUUID())
@@ -7,6 +7,7 @@ import jakarta.persistence.Id
import jakarta.persistence.Table import jakarta.persistence.Table
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
@Entity @Entity
@Table(name = "spacecraft_planning_state") @Table(name = "spacecraft_planning_state")
@@ -42,7 +43,7 @@ class SpacecraftPlanningStateEntity(
var pausedAfterReject: Boolean = false, var pausedAfterReject: Boolean = false,
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now() var updatedAt: LocalDateTime = UtcDateTimes.now()
) { ) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */ /** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(spacecraftId = "") constructor() : this(spacecraftId = "")
@@ -5,6 +5,7 @@ import jakarta.persistence.Entity
import jakarta.persistence.Id import jakarta.persistence.Id
import jakarta.persistence.Table import jakarta.persistence.Table
import java.time.LocalDateTime import java.time.LocalDateTime
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Persisted snapshot КА из НСИ, используемый как вход для refresh jobs. * Persisted snapshot КА из НСИ, используемый как вход для refresh jobs.
@@ -44,10 +45,10 @@ class SpacecraftSnapshotEntity(
var active: Boolean = true, var active: Boolean = true,
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(), var createdAt: LocalDateTime = UtcDateTimes.now(),
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now() var updatedAt: LocalDateTime = UtcDateTimes.now()
) { ) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */ /** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(spacecraftId = "", noradId = 0) constructor() : this(spacecraftId = "", noradId = 0)
@@ -5,6 +5,7 @@ import jakarta.persistence.Entity
import jakarta.persistence.Id import jakarta.persistence.Id
import jakarta.persistence.Table import jakarta.persistence.Table
import java.time.LocalDateTime import java.time.LocalDateTime
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Полный raw snapshot окон видимости КА из баллистики. * Полный raw snapshot окон видимости КА из баллистики.
@@ -40,10 +41,10 @@ class SpacecraftVisibilitySnapshotEntity(
var windowsCount: Int? = null, var windowsCount: Int? = null,
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(), var createdAt: LocalDateTime = UtcDateTimes.now(),
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now() var updatedAt: LocalDateTime = UtcDateTimes.now()
) { ) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */ /** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(spacecraftId = "", noradId = 0, payloadJson = "", payloadHash = "", fetchedAt = LocalDateTime.MIN) constructor() : this(spacecraftId = "", noradId = 0, payloadJson = "", payloadHash = "", fetchedAt = LocalDateTime.MIN)
@@ -7,6 +7,7 @@ import jakarta.persistence.Enumerated
import jakarta.persistence.Id import jakarta.persistence.Id
import jakarta.persistence.Table import jakarta.persistence.Table
import java.time.LocalDateTime import java.time.LocalDateTime
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Job на перечитывание окон видимости одного КА у баллистики. * Job на перечитывание окон видимости одного КА у баллистики.
@@ -41,10 +42,10 @@ class VisibilityRefreshJobEntity(
var rerunRequested: Boolean = false, var rerunRequested: Boolean = false,
@Column(name = "requested_at", nullable = false) @Column(name = "requested_at", nullable = false)
var requestedAt: LocalDateTime = LocalDateTime.now(), var requestedAt: LocalDateTime = UtcDateTimes.now(),
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now() var updatedAt: LocalDateTime = UtcDateTimes.now()
) { ) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */ /** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(spacecraftId = "") constructor() : this(spacecraftId = "")
@@ -13,6 +13,7 @@ import org.springframework.web.reactive.function.client.WebClientException
import org.springframework.web.reactive.function.client.WebClientResponseException import org.springframework.web.reactive.function.client.WebClientResponseException
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Клиент внешнего API с окнами видимости КА. * Клиент внешнего API с окнами видимости КА.
@@ -20,6 +21,7 @@ import java.time.temporal.ChronoUnit
@Component @Component
class ExternalPointsClient( class ExternalPointsClient(
externalApiProperties: ExternalApiProperties, externalApiProperties: ExternalApiProperties,
webClientBuilder: WebClient.Builder,
private val platformService: PlatformService, private val platformService: PlatformService,
private val planningProperties: PlanningProperties, private val planningProperties: PlanningProperties,
private val visibilityPayloadParser: VisibilityPayloadParser private val visibilityPayloadParser: VisibilityPayloadParser
@@ -27,7 +29,7 @@ class ExternalPointsClient(
private val log = LoggerFactory.getLogger(javaClass) private val log = LoggerFactory.getLogger(javaClass)
private val webClient: WebClient = WebClient.builder() private val webClient: WebClient = webClientBuilder
.baseUrl(externalApiProperties.baseUrl) .baseUrl(externalApiProperties.baseUrl)
.build() .build()
@@ -36,7 +38,7 @@ class ExternalPointsClient(
* которые разрешены для работы TGU. * которые разрешены для работы TGU.
*/ */
fun fetchAllSpacecraftPoints(): List<SpacecraftPoints> { fun fetchAllSpacecraftPoints(): List<SpacecraftPoints> {
val timeStart = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES) val timeStart = UtcDateTimes.now().truncatedTo(ChronoUnit.MINUTES)
val timeStop = timeStart.plusDays(planningProperties.externalPointsLookaheadDays) val timeStop = timeStart.plusDays(planningProperties.externalPointsLookaheadDays)
return platformService.loadAllowedPlatforms() return platformService.loadAllowedPlatforms()
@@ -93,8 +95,8 @@ class ExternalPointsClient(
.uri { uriBuilder -> .uri { uriBuilder ->
uriBuilder uriBuilder
.path("/api/satellites/{satelliteId}/rva") .path("/api/satellites/{satelliteId}/rva")
.queryParam("time_start", timeStart) .queryParam("time_start", UtcDateTimes.format(timeStart))
.queryParam("time_stop", timeStop) .queryParam("time_stop", UtcDateTimes.format(timeStop))
.build(satelliteId) .build(satelliteId)
} }
.retrieve() .retrieve()
@@ -4,6 +4,7 @@ import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import space.nstart.pcp_tgu_service.config.MissionPlaningProperties import space.nstart.pcp_tgu_service.config.MissionPlaningProperties
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -23,10 +24,25 @@ data class MissionBlankRequest(
val missionStop: LocalDateTime val missionStop: LocalDateTime
) )
@Component private data class MissionCloneOutboundRequest(
class MissionPlaningClient(properties: MissionPlaningProperties) { 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) .baseUrl(properties.baseUrl)
.build() .build()
@@ -35,7 +51,7 @@ class MissionPlaningClient(properties: MissionPlaningProperties) {
fun cloneMission(missionId: UUID, req: MissionCloneRequest): MissionCloneResponse = fun cloneMission(missionId: UUID, req: MissionCloneRequest): MissionCloneResponse =
webClient.post() webClient.post()
.uri("/api/missions/{id}/clone", missionId) .uri("/api/missions/{id}/clone", missionId)
.bodyValue(req) .bodyValue(req.toOutbound())
.retrieve() .retrieve()
.onStatus({ it.isError }) { resp -> .onStatus({ it.isError }) { resp ->
resp.bodyToMono(String::class.java).defaultIfEmpty("").map { body -> resp.bodyToMono(String::class.java).defaultIfEmpty("").map { body ->
@@ -53,11 +69,23 @@ class MissionPlaningClient(properties: MissionPlaningProperties) {
"Пустой ответ mission-planing на клон миссии $missionId" "Пустой ответ 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 = fun createBlankMission(req: MissionBlankRequest): MissionCloneResponse =
webClient.post() webClient.post()
.uri("/api/missions/blank") .uri("/api/missions/blank")
.bodyValue(req) .bodyValue(req.toOutbound())
.retrieve() .retrieve()
.onStatus({ it.isError }) { resp -> .onStatus({ it.isError }) { resp ->
resp.bodyToMono(String::class.java).defaultIfEmpty("").map { body -> resp.bodyToMono(String::class.java).defaultIfEmpty("").map { body ->
@@ -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.ObservationWindow
import space.nstart.pcp_tgu_service.domain.SpacecraftPoints import space.nstart.pcp_tgu_service.domain.SpacecraftPoints
import space.nstart.pcp_tgu_service.dto.ExternalRvaItemDto 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.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeParseException
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
/** /**
@@ -43,21 +41,11 @@ class VisibilityPayloadParser(
} }
/** /**
* Разбирает ISO-строку времени баллистики в naive `LocalDateTime`. * Разбирает ISO-строку времени баллистики в UTC-naive `LocalDateTime`.
* *
* Если время приходит с зоной (`...Z`/офсет), момент проецируем в локальный пояс JVM * Явные UTC/offset-строки нормализуются в UTC, legacy naive-строки считаются UTC-naive.
* (`ZoneId.systemDefault()`): naive-время в системе настенные часы локальной зоны, в которой * Обрезаем до микросекунд это эффективная точность persisted-планов в БД.
* работают все сервисы, без UTC-семантики. Naive-строку без зоны берём как есть.
*
* Обрезаем до микросекунд это эффективная точность системы: `planned_plan.start_time/end_time`
* хранятся как Postgres `timestamp(6)`, при записи наносекундный хвост баллистики всё равно теряется.
* Без обрезки проекция (наносекунды) и persisted-планы (микросекунды) перестают совпадать по
* бизнес-ключу ломается дедуп в read-пути и матчинг базы в [calculatePlansAfter].
*/ */
private fun toLocalDateTime(value: String): LocalDateTime = private fun toLocalDateTime(value: String): LocalDateTime =
try { UtcDateTimes.parse(value).truncatedTo(ChronoUnit.MICROS)
LocalDateTime.ofInstant(Instant.parse(value), ZoneId.systemDefault())
} catch (_: DateTimeParseException) {
LocalDateTime.parse(value)
}.truncatedTo(ChronoUnit.MICROS)
} }
@@ -5,6 +5,7 @@ import org.springframework.transaction.annotation.Transactional
import space.nstart.pcp_tgu_service.repository.AcceptedPlanSlotsUpdateJobRepository import space.nstart.pcp_tgu_service.repository.AcceptedPlanSlotsUpdateJobRepository
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Создаёт persisted job обновления слотов для accepted plan. * Создаёт persisted job обновления слотов для accepted plan.
@@ -16,6 +17,6 @@ class AcceptedPlanSlotsUpdateJobService(
/** Idempotent enqueue: один planId получает максимум одну job. */ /** Idempotent enqueue: один planId получает максимум одну job. */
@Transactional @Transactional
fun enqueue(planId: UUID) { fun enqueue(planId: UUID) {
acceptedPlanSlotsUpdateJobRepository.insertPendingIfAbsent(planId, LocalDateTime.now()) acceptedPlanSlotsUpdateJobRepository.insertPendingIfAbsent(planId, UtcDateTimes.now())
} }
} }
@@ -11,6 +11,7 @@ import space.nstart.pcp_tgu_service.repository.AcceptedPlanSlotsUpdateJobReposit
import java.net.InetAddress import java.net.InetAddress
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Scale-out safe worker, который запускает updateAcceptedPlanSlots для accepted plan. * Scale-out safe worker, который запускает updateAcceptedPlanSlots для accepted plan.
@@ -35,7 +36,7 @@ class AcceptedPlanSlotsUpdateWorker(
/** Claim видим для тестов; в production защищён FOR UPDATE SKIP LOCKED. */ /** Claim видим для тестов; в production защищён FOR UPDATE SKIP LOCKED. */
fun claimJobs(limit: Int): List<AcceptedPlanSlotsUpdateJobEntity> = fun claimJobs(limit: Int): List<AcceptedPlanSlotsUpdateJobEntity> =
transactionTemplate.execute { transactionTemplate.execute {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
val jobs = acceptedPlanSlotsUpdateJobRepository.findClaimableForUpdate(now, limit) val jobs = acceptedPlanSlotsUpdateJobRepository.findClaimableForUpdate(now, limit)
jobs.forEach { job -> jobs.forEach { job ->
job.status = AcceptedPlanSlotsUpdateJobStatus.RUNNING job.status = AcceptedPlanSlotsUpdateJobStatus.RUNNING
@@ -69,7 +70,7 @@ class AcceptedPlanSlotsUpdateWorker(
private fun markDone(planId: UUID, processInstanceKey: Long) { private fun markDone(planId: UUID, processInstanceKey: Long) {
transactionTemplate.executeWithoutResult { transactionTemplate.executeWithoutResult {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
val job = acceptedPlanSlotsUpdateJobRepository.findById(planId).orElse(null) ?: return@executeWithoutResult val job = acceptedPlanSlotsUpdateJobRepository.findById(planId).orElse(null) ?: return@executeWithoutResult
job.status = AcceptedPlanSlotsUpdateJobStatus.DONE job.status = AcceptedPlanSlotsUpdateJobStatus.DONE
job.processInstanceKey = processInstanceKey job.processInstanceKey = processInstanceKey
@@ -83,7 +84,7 @@ class AcceptedPlanSlotsUpdateWorker(
private fun markAmbiguous(planId: UUID, processInstanceKey: Long?, ex: Exception) { private fun markAmbiguous(planId: UUID, processInstanceKey: Long?, ex: Exception) {
transactionTemplate.executeWithoutResult { transactionTemplate.executeWithoutResult {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
val job = acceptedPlanSlotsUpdateJobRepository.findById(planId).orElse(null) ?: return@executeWithoutResult val job = acceptedPlanSlotsUpdateJobRepository.findById(planId).orElse(null) ?: return@executeWithoutResult
job.status = AcceptedPlanSlotsUpdateJobStatus.AMBIGUOUS job.status = AcceptedPlanSlotsUpdateJobStatus.AMBIGUOUS
job.processInstanceKey = processInstanceKey ?: job.processInstanceKey job.processInstanceKey = processInstanceKey ?: job.processInstanceKey
@@ -18,6 +18,7 @@ import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID 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) private val log = LoggerFactory.getLogger(javaClass)
fun createChainPlan(req: CreateChainPlanRequest): CreateChainPlanResponse { fun createChainPlan(req: CreateChainPlanRequest): CreateChainPlanResponse {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
if (!req.windowStop.isAfter(req.windowStart)) { if (!req.windowStop.isAfter(req.windowStart)) {
throw ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "windowStop должен быть позже windowStart") throw ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "windowStop должен быть позже windowStart")
@@ -16,6 +16,7 @@ import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Создаёт черновик плана (DRAFT) на основе существующего базового плана. * Создаёт черновик плана (DRAFT) на основе существующего базового плана.
@@ -45,7 +46,7 @@ class CreateDraftUseCase(
val base = plannedPlanRepository.findById(basePlanId) val base = plannedPlanRepository.findById(basePlanId)
.orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Plan $basePlanId not found") } .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Plan $basePlanId not found") }
val now = LocalDateTime.now() val now = UtcDateTimes.now()
return when (req.mode) { return when (req.mode) {
DraftMode.REPLACE -> createReplaceDraft(base, now) DraftMode.REPLACE -> createReplaceDraft(base, now)
@@ -9,6 +9,7 @@ import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* «Снять с учёта» / «Вернуть на учёт» терминально-негативный план. * «Снять с учёта» / «Вернуть на учёт» терминально-негативный план.
@@ -44,8 +45,8 @@ class DismissPlanUseCase(
log.info("План {} уже снят с учёта ({}) — идемпотентно", planId, plan.dismissedAt) log.info("План {} уже снят с учёта ({}) — идемпотентно", planId, plan.dismissedAt)
return return
} }
plan.dismissedAt = LocalDateTime.now() plan.dismissedAt = UtcDateTimes.now()
plan.updatedAt = LocalDateTime.now() plan.updatedAt = UtcDateTimes.now()
plannedPlanRepository.save(plan) plannedPlanRepository.save(plan)
log.info("План {} (status={}) снят с учёта", planId, plan.status) log.info("План {} (status={}) снят с учёта", planId, plan.status)
} }
@@ -59,7 +60,7 @@ class DismissPlanUseCase(
return return
} }
plan.dismissedAt = null plan.dismissedAt = null
plan.updatedAt = LocalDateTime.now() plan.updatedAt = UtcDateTimes.now()
plannedPlanRepository.save(plan) plannedPlanRepository.save(plan)
log.info("План {} (status={}) возвращён на учёт", planId, plan.status) log.info("План {} (status={}) возвращён на учёт", planId, plan.status)
} }
@@ -20,6 +20,7 @@ import space.nstart.pcp_tgu_service.repository.ProcessedPlanDecisionEventReposit
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Обрабатывает decision и закладочные feedback-события идемпотентно по eventId. * Обрабатывает decision и закладочные feedback-события идемпотентно по eventId.
@@ -132,7 +133,7 @@ class HandlePlanDecisionUseCase(
) )
} }
val now = LocalDateTime.now() val now = UtcDateTimes.now()
plan.status = PlannedPlanStatus.LAID_IN plan.status = PlannedPlanStatus.LAID_IN
plan.updatedAt = now plan.updatedAt = now
@@ -213,7 +214,7 @@ class HandlePlanDecisionUseCase(
return return
} }
val now = LocalDateTime.now() val now = UtcDateTimes.now()
plan.status = terminalStatus plan.status = terminalStatus
plan.updatedAt = now plan.updatedAt = now
plannedPlanRepository.save(plan) plannedPlanRepository.save(plan)
@@ -225,7 +226,7 @@ class HandlePlanDecisionUseCase(
message: PlanDecisionMessage, message: PlanDecisionMessage,
attempt: PlanIssueAttemptEntity attempt: PlanIssueAttemptEntity
) { ) {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
attempt.status = when (message.decision) { attempt.status = when (message.decision) {
PlanDecision.ACCEPTED -> PlanIssueAttemptStatus.ACCEPTED PlanDecision.ACCEPTED -> PlanIssueAttemptStatus.ACCEPTED
PlanDecision.REJECTED -> PlanIssueAttemptStatus.REJECTED PlanDecision.REJECTED -> PlanIssueAttemptStatus.REJECTED
@@ -241,7 +242,7 @@ class HandlePlanDecisionUseCase(
attempt: PlanIssueAttemptEntity, attempt: PlanIssueAttemptEntity,
plan: PlannedPlanEntity plan: PlannedPlanEntity
) { ) {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
recordDecisionOnAttempt(message, attempt) recordDecisionOnAttempt(message, attempt)
plan.status = PlannedPlanStatus.AWAITING_LAYIN plan.status = PlannedPlanStatus.AWAITING_LAYIN
@@ -262,7 +263,7 @@ class HandlePlanDecisionUseCase(
plan: PlannedPlanEntity, plan: PlannedPlanEntity,
state: SpacecraftPlanningStateEntity state: SpacecraftPlanningStateEntity
) { ) {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
recordDecisionOnAttempt(message, attempt) recordDecisionOnAttempt(message, attempt)
plan.status = PlannedPlanStatus.REJECTED plan.status = PlannedPlanStatus.REJECTED
@@ -307,7 +308,7 @@ class HandlePlanDecisionUseCase(
} }
private fun claimEvent(eventId: java.util.UUID): Boolean = 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 { private fun getOrCreateLockedState(spacecraftId: String): SpacecraftPlanningStateEntity {
spacecraftPlanningStateJpaRepository.findById(spacecraftId).orElse(null) spacecraftPlanningStateJpaRepository.findById(spacecraftId).orElse(null)
@@ -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.ProcessedPlanDecisionEventRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import java.time.LocalDateTime import java.time.LocalDateTime
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* На изменение параметров станций перезапрашивает ЗРВ у баллистики по всем активным КА. * На изменение параметров станций перезапрашивает ЗРВ у баллистики по всем активным КА.
@@ -25,7 +26,7 @@ class HandleStationParamsChangedUseCase(
/** Идемпотентно по eventId; ставит refresh job по всем активным КА. */ /** Идемпотентно по eventId; ставит refresh job по всем активным КА. */
@Transactional @Transactional
fun handle(message: StationParamsChangedMessage) { fun handle(message: StationParamsChangedMessage) {
val claimed = processedPlanDecisionEventRepository.insertIfAbsent(message.eventId, LocalDateTime.now()) != 0 val claimed = processedPlanDecisionEventRepository.insertIfAbsent(message.eventId, UtcDateTimes.now()) != 0
if (!claimed) { if (!claimed) {
return return
} }
@@ -20,6 +20,7 @@ import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaReposit
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Выдаёт дозревшие планы. В on-demand-модели голова цепочки считается из снапшота ЗРВ * Выдаёт дозревшие планы. В on-demand-модели голова цепочки считается из снапшота ЗРВ
@@ -83,7 +84,7 @@ class IssueDuePlanWorker(
/** Готовит выдачу головы проекции для КА под lock'ом состояния (scale-out safe). */ /** Готовит выдачу головы проекции для КА под lock'ом состояния (scale-out safe). */
private fun prepareDueForSpacecraft(spacecraftId: String): PreparedAttempt? { private fun prepareDueForSpacecraft(spacecraftId: String): PreparedAttempt? {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
val state = getOrCreateLockedState(spacecraftId) val state = getOrCreateLockedState(spacecraftId)
// Форвард-цепочку не выдаём, пока по КА есть выдача «в работе»: ISSUING или WAITING_DECISION // Форвард-цепочку не выдаём, пока по КА есть выдача «в работе»: ISSUING или WAITING_DECISION
// в пределах grace после старта. Прошедший grace больше не блокирует — молчаливо продолжаем // в пределах grace после старта. Прошедший grace больше не блокирует — молчаливо продолжаем
@@ -154,7 +155,7 @@ class IssueDuePlanWorker(
.maxByOrNull { plan -> plan.endTime }?.planId ?: fallback .maxByOrNull { plan -> plan.endTime }?.planId ?: fallback
private fun prepareSpecificPlan(planId: UUID): PreparedAttempt? { private fun prepareSpecificPlan(planId: UUID): PreparedAttempt? {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
val plan = plannedPlanRepository.findById(planId).orElse(null) ?: return null val plan = plannedPlanRepository.findById(planId).orElse(null) ?: return null
val state = getOrCreateLockedState(plan.spacecraftId) val state = getOrCreateLockedState(plan.spacecraftId)
assertReissuableInSlot(plan, now) assertReissuableInSlot(plan, now)
@@ -176,7 +177,7 @@ class IssueDuePlanWorker(
} }
private fun prepareDraftPromotion(draftPlanId: UUID): PreparedAttempt? { private fun prepareDraftPromotion(draftPlanId: UUID): PreparedAttempt? {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
val plan = plannedPlanRepository.findById(draftPlanId).orElse(null) ?: return null val plan = plannedPlanRepository.findById(draftPlanId).orElse(null) ?: return null
if (plan.status != PlannedPlanStatus.DRAFT) { if (plan.status != PlannedPlanStatus.DRAFT) {
throw ResponseStatusException( throw ResponseStatusException(
@@ -326,7 +327,7 @@ class IssueDuePlanWorker(
private fun markStarted(response: ProcessStartResponse) { private fun markStarted(response: ProcessStartResponse) {
transactionTemplate.executeWithoutResult { transactionTemplate.executeWithoutResult {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
val attempt = planIssueAttemptRepository.findById(response.attemptId).orElse(null) ?: return@executeWithoutResult val attempt = planIssueAttemptRepository.findById(response.attemptId).orElse(null) ?: return@executeWithoutResult
val plan = plannedPlanRepository.findById(response.planId).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) { private fun markAmbiguous(preparedAttempt: PreparedAttempt) {
transactionTemplate.executeWithoutResult { transactionTemplate.executeWithoutResult {
val now = LocalDateTime.now() val now = UtcDateTimes.now()
val attempt = planIssueAttemptRepository.findById(preparedAttempt.attemptId).orElse(null) ?: return@executeWithoutResult val attempt = planIssueAttemptRepository.findById(preparedAttempt.attemptId).orElse(null) ?: return@executeWithoutResult
val plan = plannedPlanRepository.findById(preparedAttempt.planId).orElse(null) ?: return@executeWithoutResult val plan = plannedPlanRepository.findById(preparedAttempt.planId).orElse(null) ?: return@executeWithoutResult
attempt.status = PlanIssueAttemptStatus.AMBIGUOUS attempt.status = PlanIssueAttemptStatus.AMBIGUOUS
@@ -12,6 +12,7 @@ import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Read-сервис планов: объединяет persisted (выданные, история вечно) с вычисленной * Read-сервис планов: объединяет persisted (выданные, история вечно) с вычисленной
@@ -80,7 +81,7 @@ class PlanQueryService(
.sortedBy { response -> response.startTime } .sortedBy { response -> response.startTime }
private fun historySince(historyDays: Long): LocalDateTime = private fun historySince(historyDays: Long): LocalDateTime =
LocalDateTime.now().minusDays(historyDays.coerceAtLeast(0)) UtcDateTimes.now().minusDays(historyDays.coerceAtLeast(0))
private fun PlannedPlanEntity.businessKey(): String = private fun PlannedPlanEntity.businessKey(): String =
"$spacecraftId|$startTime|$endTime|$kppId" "$spacecraftId|$startTime|$endTime|$kppId"
@@ -7,6 +7,7 @@ import space.nstart.pcp_tgu_service.integration.api.VisibilityPayloadParser
import space.nstart.pcp_tgu_service.repository.SpacecraftVisibilitySnapshotRepository import space.nstart.pcp_tgu_service.repository.SpacecraftVisibilitySnapshotRepository
import java.security.MessageDigest import java.security.MessageDigest
import java.time.LocalDateTime import java.time.LocalDateTime
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
/** /**
* Сохраняет raw visibility payload. Расписание считается on-demand из снапшота * Сохраняет raw visibility payload. Расписание считается on-demand из снапшота
@@ -25,7 +26,7 @@ class SpacecraftVisibilitySnapshotService(
val payloadHash = sha256(payloadJson) val payloadHash = sha256(payloadJson)
val existing = spacecraftVisibilitySnapshotRepository.findById(spacecraftId).orElse(null) val existing = spacecraftVisibilitySnapshotRepository.findById(spacecraftId).orElse(null)
val metadata = runCatching { extractMetadata(spacecraftId, payloadJson) }.getOrNull() val metadata = runCatching { extractMetadata(spacecraftId, payloadJson) }.getOrNull()
val now = LocalDateTime.now() val now = UtcDateTimes.now()
if (existing == null) { if (existing == null) {
spacecraftVisibilitySnapshotRepository.save( spacecraftVisibilitySnapshotRepository.save(

Some files were not shown because too many files have changed in this diff Show More