From fe54c095abc4e18dc59a9d4792a32cad25974a5f Mon Sep 17 00:00:00 2001 From: emelianov Date: Thu, 25 Jun 2026 16:21:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=BE=D1=80=D0=BC=D0=B0=D1=82=20=D0=B2?= =?UTF-8?q?=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8=20=D0=B2=20UTC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orbitalPoints/timeStepper/TLEStepper.kt | 2 +- .../ballistics/utils/astro/AstronomerJ2000.kt | 22 +++--- libs/ballistics-lib/src/test/kotlin/Test.kt | 2 +- .../ballistics/ExactTimePositionRequestDTO.kt | 3 +- .../dto/ballistics/FlightLineDTO.kt | 3 +- .../dto/ballistics/IcRvaRequestDTO.kt | 5 +- .../dto/ballistics/IntervalDTO.kt | 5 +- .../dto/ballistics/ObjViewRequestDTO.kt | 5 +- .../dto/ballistics/OrbPointDTO.kt | 3 +- .../dto/ballistics/PointViewParamDTO.kt | 3 +- .../SatelliteOrbitAvailabilityDTO.kt | 5 +- .../dto/ballistics/SquareViewParamDTO.kt | 5 +- .../dto/ballistics/TargetPositionDTO.kt | 3 +- .../dto/ballistics/TlePointRequestDTO.kt | 3 +- .../dto/ballistics/TleRvaRequestDTO.kt | 5 +- .../dto/coverage/CoverageCandidateDebugDTO.kt | 5 +- .../CoverageSchemeCalculateRequestDTO.kt | 5 +- .../EarthCellCalculationRequestDTO.kt | 5 +- .../slots/BookedSlotsStatusChangedEvent.kt | 3 +- .../dto/satellite/mission/DropRouteDTOs.kt | 5 +- .../satellite/mission/SurveyRouteBuildDTOs.kt | 3 +- .../dto/satellite/modes/SurveyModeInfoDTO.kt | 5 +- .../pcp/pcp_types_lib/message/KafkaMessage.kt | 3 +- .../pcp/pcp_types_lib/utils/UtcDateTimes.kt | 60 ++++++++++++++++ .../json/LocalDateTimeTimeZoneSerializer.kt | 16 ++--- .../pcp_types_lib/utils/UtcDateTimesTest.kt | 29 ++++++++ .../angular/AngularMotionController.kt | 16 +++-- .../configuration/TimeConfiguration.kt | 12 ++++ .../configuration/UtcTimeJsonConfiguration.kt | 40 +++++++++++ .../controller/SatelliteController.kt | 42 +++++++---- .../entity/AscNodeEntity.kt | 3 +- .../entity/EarthCellViewsEntity.kt | 5 +- .../entity/EarthCoverageEntity.kt | 5 +- .../entity/FlightLineEntity.kt | 3 +- .../pcp_request_service/entity/PDCMEntity.kt | 3 +- .../entity/SatelliteInitialConditionEntity.kt | 12 ++-- .../SatelliteInitialConditionService.kt | 9 ++- .../service/SatelliteService.kt | 16 +++-- .../pcp_request_service/utils/UtcDateTimes.kt | 59 +++++++++++++++ .../utils/UtcDateTimesTest.kt | 40 +++++++++++ .../configuration/UtcTimeJsonConfiguration.kt | 51 +++++++++++++ .../entity/ComplexPlanRunEntity.kt | 7 +- .../entity/SatelliteModeEntity.kt | 5 +- .../entity/SatelliteModeSnapshotEntity.kt | 7 +- .../model/mode/SurveyMode.kt | 5 +- .../service/SlotService.kt | 13 ++-- .../client/BallisticsClient.kt | 5 +- .../configuration/UtcTimeConfiguration.kt | 55 ++++++++++++++ .../space/nstart/pcp/complan/JacksonConfig.kt | 28 ++++++++ .../pcp/complan/apl/BasicPlanCalculator.kt | 3 +- .../complan/config/UtcTimeConfiguration.kt | 55 ++++++++++++++ .../db/entity/DynamicPlanRouteEntity.kt | 5 +- .../complan/db/entity/DynamicPlanRunEntity.kt | 3 +- .../pcp/complan/service/BallTaskServce.kt | 28 +++++--- .../complan/service/DynamicPlanRunService.kt | 11 +-- .../pcp/complan/spacecraft/SpaceCraft.kt | 5 +- .../configuration/Jackson2UtcConfiguration.kt | 41 +++++++++++ .../configuration/UtcTimeConfiguration.kt | 55 ++++++++++++++ .../service/BallisticsService.kt | 5 +- .../service/BookedSlotsEventPublisher.kt | 8 ++- .../service/ComplexMissionService.kt | 5 +- .../service/MissionPlaningService.kt | 9 ++- .../requestservice/config/JacksonConfig.kt | 27 +++++++ .../config/UtcTimeConfiguration.kt | 55 ++++++++++++++ .../controller/RequestController.kt | 17 +++-- .../service/GridRebuildService.kt | 5 +- .../service/OutboxPublisherService.kt | 5 +- .../service/RequestExpiryService.kt | 5 +- .../requestservice/service/RequestService.kt | 7 +- .../service/RouteMatchingService.kt | 5 +- .../config/UtcTimeConfiguration.kt | 55 ++++++++++++++ .../service/RoutePassportIngestionService.kt | 3 +- .../service/RoutePassportJsonParserService.kt | 6 +- .../service/RoutePassportOutboxService.kt | 7 +- .../configuration/UtcTimeConfiguration.kt | 55 ++++++++++++++ .../config/CamundaJacksonConfig.kt | 28 ++++++++ .../config/UtcTimeConfiguration.kt | 68 ++++++++++++++++++ .../controller/TestTguController.kt | 9 +-- .../pcp_tgu_service/dto/PlanResponse.kt | 8 +-- .../AcceptedPlanSlotsUpdateJobEntity.kt | 5 +- .../entity/PlanIssueAttemptEntity.kt | 5 +- .../entity/PlannedPlanEntity.kt | 5 +- .../ProcessedPlanDecisionEventEntity.kt | 3 +- .../entity/SpacecraftPlanningStateEntity.kt | 3 +- .../entity/SpacecraftSnapshotEntity.kt | 5 +- .../SpacecraftVisibilitySnapshotEntity.kt | 5 +- .../entity/VisibilityRefreshJobEntity.kt | 5 +- .../integration/api/ExternalPointsClient.kt | 10 +-- .../integration/api/MissionPlaningClient.kt | 38 ++++++++-- .../api/VisibilityPayloadParser.kt | 22 ++---- .../AcceptedPlanSlotsUpdateJobService.kt | 3 +- .../service/AcceptedPlanSlotsUpdateWorker.kt | 7 +- .../service/CreateChainPlanUseCase.kt | 3 +- .../service/CreateDraftUseCase.kt | 3 +- .../service/DismissPlanUseCase.kt | 7 +- .../service/HandlePlanDecisionUseCase.kt | 13 ++-- .../HandleStationParamsChangedUseCase.kt | 3 +- .../service/IssueDuePlanWorker.kt | 11 +-- .../service/PlanQueryService.kt | 3 +- .../SpacecraftVisibilitySnapshotService.kt | 3 +- .../service/SyncSpacecraftFromNsiUseCase.kt | 11 ++- .../service/TestPlanDecisionService.kt | 5 +- .../service/VisibilityRefreshJobService.kt | 3 +- .../service/VisibilityRefreshWorker.kt | 13 ++-- .../dto/PlanResponseContractTest.kt | 24 +++++-- .../api/VisibilityPayloadParserTest.kt | 10 ++- .../src/api/ballisticsApi.ts | 12 ++-- .../src/api/complexPlanApi.ts | 12 ++-- .../src/api/missionPlanningApi.ts | 14 ++-- .../pcp-tgu-ui-service/src/api/tguPlanApi.ts | 4 +- .../tgu-complex-plan/TguComplexPlanTab.tsx | 4 +- .../src/features/tgu-editor/TguEditorTab.tsx | 24 +++---- .../src/features/tgu-map-2d/Tgu2DMapTab.tsx | 6 +- .../tgu-planning/createChainWindows.test.ts | 18 ++--- .../tgu-planning/createChainWindows.ts | 16 ++--- .../features/tgu-requests/TguRequestsTab.tsx | 8 +-- .../src/features/tgu-slots/TguSlotsTab.tsx | 8 +-- .../pcp-tgu-ui-service/src/utils/localTime.ts | 27 ++++--- .../configuration/UtcTimeConfiguration.kt | 55 ++++++++++++++ .../controller/BookingsController.kt | 5 +- .../controller/CatalogRestController.kt | 7 +- .../controller/DynamicPlanController.kt | 5 +- .../slots_service/controller/MapController.kt | 2 +- .../controller/TleComparisonController.kt | 5 +- .../pcp/slots_service/czml/CZMLClock.kt | 10 +-- .../nstart/pcp/slots_service/czml/CZMLPath.kt | 41 ++++------- .../service/BallisticsService.kt | 5 +- .../pcp/slots_service/service/CZMLService.kt | 11 +-- .../service/CoverageSchemeService.kt | 5 +- .../service/DynamicPlanService.kt | 11 +-- .../slots_service/service/MissionService.kt | 3 +- .../service/SatellitePdcmService.kt | 5 +- .../pcp/slots_service/service/SlotService.kt | 15 ++-- .../slots_service/service/StationService.kt | 9 +++ .../static/angular_motion_scripts.js | 26 ++++--- .../main/resources/static/bookings_scripts.js | 19 ++--- .../main/resources/static/catalog_scripts.js | 16 ++--- .../main/resources/static/cesium_scripts.js | 69 ++++++++++-------- .../resources/static/complex_plan_scripts.js | 32 ++------- .../resources/static/css/angular-motion.css | 28 +++++++- .../resources/static/current_plans_scripts.js | 25 ++----- .../static/dynamic_plan_map_layers.js | 10 ++- .../resources/static/dynamic_plan_scripts.js | 2 +- .../resources/static/group_state_scripts.js | 9 +-- .../main/resources/static/requests_scripts.js | 48 ++++++------- .../src/main/resources/static/rva_scripts.js | 7 +- .../static/satellite_pdcm_scripts.js | 24 +++---- .../resources/static/tgu_planning_scripts.js | 12 ++-- .../resources/static/tle_analysis_scripts.js | 3 +- .../static/tle_comparison_scripts.js | 28 +++++--- .../src/main/resources/static/utc_time.js | 72 +++++++++++++++++++ .../templates/fragments/base/general.html | 7 +- .../src/main/resources/templates/map.html | 21 +++--- .../configuration/UtcTimeConfiguration.kt | 55 ++++++++++++++ .../controller/BookingController.kt | 5 +- .../slots_service/entity/SatelliteIcEntity.kt | 3 +- .../pcp/slots_service/entity/SlotEntity.kt | 5 +- .../nstart/pcp/slots_service/model/Mar.kt | 5 +- .../configuration/Jackson2UtcConfiguration.kt | 41 +++++++++++ .../configuration/UtcTimeConfiguration.kt | 55 ++++++++++++++ .../controller/TLEController.kt | 5 ++ .../entity/TLEEntity.kt | 3 +- .../service/CelesTrakService.kt | 9 ++- .../service/TlePollingSettingsService.kt | 10 ++- 164 files changed, 1847 insertions(+), 634 deletions(-) create mode 100644 libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/UtcDateTimes.kt create mode 100644 libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/utils/UtcDateTimesTest.kt create mode 100644 services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/configuration/TimeConfiguration.kt create mode 100644 services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/configuration/UtcTimeJsonConfiguration.kt create mode 100644 services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/utils/UtcDateTimes.kt create mode 100644 services/pcp-ballistics-service/src/test/kotlin/space/nstart/pcp/pcp_request_service/utils/UtcDateTimesTest.kt create mode 100644 services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/configuration/UtcTimeJsonConfiguration.kt create mode 100644 services/pcp-coverage-scheme-service/src/main/kotlin/space/nstart/pcp/pcp_coverage_scheme_service/configuration/UtcTimeConfiguration.kt create mode 100644 services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/config/UtcTimeConfiguration.kt create mode 100644 services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/configuration/Jackson2UtcConfiguration.kt create mode 100644 services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/configuration/UtcTimeConfiguration.kt create mode 100644 services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/config/UtcTimeConfiguration.kt create mode 100644 services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/config/UtcTimeConfiguration.kt create mode 100644 services/pcp-satellite-catalog-service/src/main/kotlin/space/nstart/pcp/pcp_satellite_catalog_service/configuration/UtcTimeConfiguration.kt create mode 100644 services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/UtcTimeConfiguration.kt create mode 100644 services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/configuration/UtcTimeConfiguration.kt create mode 100644 services/pcp-ui-service/src/main/resources/static/utc_time.js create mode 100644 services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/configuration/UtcTimeConfiguration.kt create mode 100644 services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/configuration/Jackson2UtcConfiguration.kt create mode 100644 services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/configuration/UtcTimeConfiguration.kt diff --git a/libs/ballistics-lib/src/main/kotlin/ballistics/orbitalPoints/timeStepper/TLEStepper.kt b/libs/ballistics-lib/src/main/kotlin/ballistics/orbitalPoints/timeStepper/TLEStepper.kt index 8b74501..48a2c22 100644 --- a/libs/ballistics-lib/src/main/kotlin/ballistics/orbitalPoints/timeStepper/TLEStepper.kt +++ b/libs/ballistics-lib/src/main/kotlin/ballistics/orbitalPoints/timeStepper/TLEStepper.kt @@ -22,7 +22,7 @@ class TLEStepper(str1: String, str2: String, earthType: EarthType) : AbstractSte private val ascendingNodeCache = mutableMapOf() init { - baseEpoch = extractUTCMillis(satellite) / 1000.0 + 10800 + baseEpoch = extractUTCMillis(satellite) / 1000.0 nextAscendingNodeAfterEpoch = findNextAscendingNodeAfterEpoch() ascendingNodeCache[0] = nextAscendingNodeAfterEpoch } diff --git a/libs/ballistics-lib/src/main/kotlin/ballistics/utils/astro/AstronomerJ2000.kt b/libs/ballistics-lib/src/main/kotlin/ballistics/utils/astro/AstronomerJ2000.kt index b636e0a..7b486f7 100644 --- a/libs/ballistics-lib/src/main/kotlin/ballistics/utils/astro/AstronomerJ2000.kt +++ b/libs/ballistics-lib/src/main/kotlin/ballistics/utils/astro/AstronomerJ2000.kt @@ -26,9 +26,9 @@ class AstronomerJ2000(var earthType: EarthType) { var d = tDtTm / 86400.0 var dat = truncate(d) var tt = d - dat - if (tt < 0.125) { - dat-- - } +// if (tt < 0.125) { +// dat-- +// } var t = (dat - baseDate) / (36525.0) var t2 = t * t @@ -69,11 +69,11 @@ class AstronomerJ2000(var earthType: EarthType) { psi += r - if (tt < 0.125) { - tt += 0.875 - } else { - tt -= 0.125 - } +// if (tt < 0.125) { +// tt += 0.875 +// } else { +// tt -= 0.125 +// } r = 84381.448e0 - 46.84024e0 * t - 0.00059e0 * t2 + 0.001813e0 * t3 r = r * PI / 180.0 / 3600.0 @@ -172,7 +172,7 @@ class AstronomerJ2000(var earthType: EarthType) { fun moonCoordinates(tm: Double): Vector3D { 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 t2 = t * t val t3 = t2 * t @@ -241,8 +241,8 @@ class AstronomerJ2000(var earthType: EarthType) { } fun sunCoordinates(tm: Double): Vector3D { - var d = tm / 86400.0 - var dd = d - 0.125 + var dd = tm / 86400.0 +// var dd = d - 0.125 var t = (dd - baseDate) / 36525.0 var t2 = t * t var t3 = t2 * t diff --git a/libs/ballistics-lib/src/test/kotlin/Test.kt b/libs/ballistics-lib/src/test/kotlin/Test.kt index f119bbf..1f543d6 100644 --- a/libs/ballistics-lib/src/test/kotlin/Test.kt +++ b/libs/ballistics-lib/src/test/kotlin/Test.kt @@ -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( fromDateTime(tt2), diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/ExactTimePositionRequestDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/ExactTimePositionRequestDTO.kt index af4b454..2f7188e 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/ExactTimePositionRequestDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/ExactTimePositionRequestDTO.kt @@ -2,11 +2,12 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.DecimalMin +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @Schema(description = "Запрос на расчет параметров выхода на заданное время") class ExactTimePositionRequestDTO( - val time : LocalDateTime = LocalDateTime.now(), + val time : LocalDateTime = UtcDateTimes.now(), val timeStop : LocalDateTime? = null, @field:DecimalMin(value = "1", message = "Шаг не может быть меньше 1 мс") diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/FlightLineDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/FlightLineDTO.kt index 8a6b066..06c6249 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/FlightLineDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/FlightLineDTO.kt @@ -2,6 +2,7 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics import com.fasterxml.jackson.annotation.JsonProperty import io.swagger.v3.oas.annotations.media.Schema +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @Schema(description = "Модель данных параметров трассы полета и полосы обзора") @@ -12,7 +13,7 @@ class FlightLineDTO( required = true, nullable = false ) - val time : LocalDateTime = LocalDateTime.now(), + val time : LocalDateTime = UtcDateTimes.now(), @JsonProperty @field:Schema( description = "Виток" diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/IcRvaRequestDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/IcRvaRequestDTO.kt index c34d328..64f53e8 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/IcRvaRequestDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/IcRvaRequestDTO.kt @@ -2,6 +2,7 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics import com.fasterxml.jackson.annotation.JsonProperty import jakarta.validation.constraints.NotEmpty +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class IcRvaRequestDTO( @@ -13,8 +14,8 @@ class IcRvaRequestDTO( @field:NotEmpty(message = "Массив станций приема") val station: StationDTO = StationDTO(), - val timeStart : LocalDateTime = LocalDateTime.now(), + val timeStart : LocalDateTime = UtcDateTimes.now(), - val timeStop : LocalDateTime = LocalDateTime.now() + val timeStop : LocalDateTime = UtcDateTimes.now() ) { } \ No newline at end of file diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/IntervalDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/IntervalDTO.kt index a5f46f3..4b38313 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/IntervalDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/IntervalDTO.kt @@ -1,10 +1,11 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class IntervalDTO( - val begin : LocalDateTime = LocalDateTime.now(), - val end : LocalDateTime = LocalDateTime.now(), + val begin : LocalDateTime = UtcDateTimes.now(), + val end : LocalDateTime = UtcDateTimes.now(), val durationSec : Double = 0.0 ) { } diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/ObjViewRequestDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/ObjViewRequestDTO.kt index 3253a0c..680d6bc 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/ObjViewRequestDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/ObjViewRequestDTO.kt @@ -1,11 +1,12 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class ObjViewRequestDTO( val satellites : List = listOf(), - val timeStart : LocalDateTime = LocalDateTime.now(), - val timeStop : LocalDateTime = LocalDateTime.now(), + val timeStart : LocalDateTime = UtcDateTimes.now(), + val timeStop : LocalDateTime = UtcDateTimes.now(), val obj : ObjDTO = ObjDTO(), val sunAngleMin : Double? = null, val rollMin : Double? = null, diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/OrbPointDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/OrbPointDTO.kt index 7a8d81b..f68d06a 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/OrbPointDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/OrbPointDTO.kt @@ -1,9 +1,10 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class OrbPointDTO( - val time : LocalDateTime = LocalDateTime.now(), + val time : LocalDateTime = UtcDateTimes.now(), val revolution : Long = 0, val vx : Double = 0.0, val vy : Double = 0.0, diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/PointViewParamDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/PointViewParamDTO.kt index 666deb8..d2da315 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/PointViewParamDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/PointViewParamDTO.kt @@ -1,11 +1,12 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class PointViewParamDTO( val noradId : Long = 0, val objectId : String = "", - val time : LocalDateTime = LocalDateTime.now(), + val time : LocalDateTime = UtcDateTimes.now(), val revolution : Long = 0, val gamma : Double = 0.0, val sunAngle : Double = 0.0, diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/SatelliteOrbitAvailabilityDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/SatelliteOrbitAvailabilityDTO.kt index 9ffcc8a..4f664b6 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/SatelliteOrbitAvailabilityDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/SatelliteOrbitAvailabilityDTO.kt @@ -1,10 +1,11 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class SatelliteOrbitAvailabilityDTO( val satelliteId: Long = 0, - val timeStart: LocalDateTime = LocalDateTime.now(), - val timeStop: LocalDateTime = LocalDateTime.now() + val timeStart: LocalDateTime = UtcDateTimes.now(), + val timeStop: LocalDateTime = UtcDateTimes.now() ) { } diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/SquareViewParamDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/SquareViewParamDTO.kt index d194b96..b244d78 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/SquareViewParamDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/SquareViewParamDTO.kt @@ -1,5 +1,6 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @@ -8,10 +9,10 @@ class SquareViewParamDTO( val noradId : Long = 0, val objectId : String = "", val revolutionBegin : Long = 0, - val timeBegin : LocalDateTime = LocalDateTime.now(), + val timeBegin : LocalDateTime = UtcDateTimes.now(), val revSignBegin : RevolutionSign = RevolutionSign.ASC, val revolutionEnd: Long = 0, - val timeEnd : LocalDateTime = LocalDateTime.now(), + val timeEnd : LocalDateTime = UtcDateTimes.now(), val revSignEnd : RevolutionSign = RevolutionSign.ASC, val gammaMin : Double = 0.0, val gammaMax : Double = 0.0 diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TargetPositionDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TargetPositionDTO.kt index 5efc91f..9166a50 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TargetPositionDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TargetPositionDTO.kt @@ -1,12 +1,13 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics import io.swagger.v3.oas.annotations.media.Schema +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @Schema(description = "Модель данных целеуказаний") class TargetPositionDTO( - val time : LocalDateTime = LocalDateTime.now(), + val time : LocalDateTime = UtcDateTimes.now(), val elevation : Double = 0.0, val azimuth : Double = 0.0, val range : Double = 0.0 diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TlePointRequestDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TlePointRequestDTO.kt index bdb30e3..f57f726 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TlePointRequestDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TlePointRequestDTO.kt @@ -1,8 +1,9 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class TlePointRequestDTO( var tle: TLEDTO = TLEDTO(), - var time: LocalDateTime = LocalDateTime.now() + var time: LocalDateTime = UtcDateTimes.now() ) diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TleRvaRequestDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TleRvaRequestDTO.kt index 050ae8e..2615706 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TleRvaRequestDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TleRvaRequestDTO.kt @@ -2,6 +2,7 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics import com.fasterxml.jackson.annotation.JsonProperty import jakarta.validation.constraints.NotEmpty +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class TleRvaRequestDTO( @@ -11,9 +12,9 @@ class TleRvaRequestDTO( @field:NotEmpty(message = "Массив станций приема") val station: StationDTO = StationDTO(), - val timeStart : LocalDateTime = LocalDateTime.now(), + val timeStart : LocalDateTime = UtcDateTimes.now(), - val timeStop : LocalDateTime = LocalDateTime.now() + val timeStop : LocalDateTime = UtcDateTimes.now() ) { } \ No newline at end of file diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/coverage/CoverageCandidateDebugDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/coverage/CoverageCandidateDebugDTO.kt index 42717a4..bc8f62c 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/coverage/CoverageCandidateDebugDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/coverage/CoverageCandidateDebugDTO.kt @@ -1,11 +1,12 @@ package space.nstart.pcp.pcp_types_lib.dto.coverage +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class CoverageCandidateDebugDTO( val satelliteId: Long = 0, - val timeStart: LocalDateTime = LocalDateTime.now(), - val timeEnd: LocalDateTime = LocalDateTime.now(), + val timeStart: LocalDateTime = UtcDateTimes.now(), + val timeEnd: LocalDateTime = UtcDateTimes.now(), val rollDegrees: Double = 0.0, val accepted: Boolean = false, val reason: String = "", diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/coverage/CoverageSchemeCalculateRequestDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/coverage/CoverageSchemeCalculateRequestDTO.kt index 478ba83..b1b1f2a 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/coverage/CoverageSchemeCalculateRequestDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/coverage/CoverageSchemeCalculateRequestDTO.kt @@ -1,12 +1,13 @@ package space.nstart.pcp.pcp_types_lib.dto.coverage +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class CoverageSchemeCalculateRequestDTO( val polygonWkt: String = "", val satelliteIds: List = listOf(), - val timeStart: LocalDateTime = LocalDateTime.now(), - val timeEnd: LocalDateTime = LocalDateTime.now(), + val timeStart: LocalDateTime = UtcDateTimes.now(), + val timeEnd: LocalDateTime = UtcDateTimes.now(), val rollStepDegrees: Double? = null, val minimumTechnologyOverlap: Double? = null, val strictContinuousCoverage: Boolean = true, diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/requests/EarthCellCalculationRequestDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/requests/EarthCellCalculationRequestDTO.kt index 72afb39..e403ee9 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/requests/EarthCellCalculationRequestDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/requests/EarthCellCalculationRequestDTO.kt @@ -1,10 +1,11 @@ package space.nstart.pcp.pcp_types_lib.dto.requests +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class EarthCellCalculationRequestDTO( - val timeStart : LocalDateTime = LocalDateTime.now(), - val timeStop : LocalDateTime = LocalDateTime.now(), + val timeStart : LocalDateTime = UtcDateTimes.now(), + val timeStop : LocalDateTime = UtcDateTimes.now(), val cell : EarthCellWithRequestsDTO = EarthCellWithRequestsDTO() ) { } \ No newline at end of file diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/requests/slots/BookedSlotsStatusChangedEvent.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/requests/slots/BookedSlotsStatusChangedEvent.kt index f902401..a04114c 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/requests/slots/BookedSlotsStatusChangedEvent.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/requests/slots/BookedSlotsStatusChangedEvent.kt @@ -1,12 +1,13 @@ 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.util.UUID data class BookedSlotsStatusChangedEvent( val bookedSlotIds: List = emptyList(), val status: BookedSlotStatus = BookedSlotStatus.BOOKED, - val occurredAt: LocalDateTime = LocalDateTime.now(), + val occurredAt: LocalDateTime = UtcDateTimes.now(), val sourceService: String = "", val missionId: UUID? = null, val modeId: Long? = null, diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/DropRouteDTOs.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/DropRouteDTOs.kt index ff21636..d2dc3ac 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/DropRouteDTOs.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/DropRouteDTOs.kt @@ -1,6 +1,7 @@ package space.nstart.pcp.pcp_types_lib.dto.satellite.mission import io.swagger.v3.oas.annotations.media.Schema +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime /** @@ -23,10 +24,10 @@ data class DropRouteSaveRequestDTO( val revolution: Long = 0, @field:Schema(description = "Начало зоны радиовидимости (локальное время без зоны)") - val zoneStart: LocalDateTime = LocalDateTime.now(), + val zoneStart: LocalDateTime = UtcDateTimes.now(), @field:Schema(description = "Конец зоны радиовидимости (локальное время без зоны)") - val zoneStop: LocalDateTime = LocalDateTime.now(), + val zoneStop: LocalDateTime = UtcDateTimes.now(), @field:Schema(description = "Идентификаторы режимов съёмки (survey_modes), которые сбрасываются в этой зоне") val surveyModeIds: List = emptyList(), diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/SurveyRouteBuildDTOs.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/SurveyRouteBuildDTOs.kt index cbde248..7414b6c 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/SurveyRouteBuildDTOs.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/SurveyRouteBuildDTOs.kt @@ -3,6 +3,7 @@ package space.nstart.pcp.pcp_types_lib.dto.satellite.mission import com.fasterxml.jackson.annotation.JsonProperty import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.DecimalMin +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime /** @@ -22,7 +23,7 @@ data class SurveyRouteBuildRequestDTO( val satelliteId: Long = 0, @field:Schema(description = "Момент видимости (локальное время без зоны)") - val time: LocalDateTime = LocalDateTime.now(), + val time: LocalDateTime = UtcDateTimes.now(), @field:DecimalMin(value = "1", message = "Длительность съёмки должна быть не меньше 1 секунды") @field:Schema(description = "Длительность съёмки, секунды") diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/modes/SurveyModeInfoDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/modes/SurveyModeInfoDTO.kt index 73a4b46..e765adf 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/modes/SurveyModeInfoDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/modes/SurveyModeInfoDTO.kt @@ -1,11 +1,12 @@ package space.nstart.pcp.pcp_types_lib.dto.satellite.modes +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime data class SurveyModeInfoDTO( val revolution : Long = 0, - val time : LocalDateTime = LocalDateTime.now(), - val timStop : LocalDateTime = LocalDateTime.now(), + val time : LocalDateTime = UtcDateTimes.now(), + val timStop : LocalDateTime = UtcDateTimes.now(), val duration : Double = 0.0, val roll : Double = 0.0, val contourWKT : String = "", diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/message/KafkaMessage.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/message/KafkaMessage.kt index 0e55531..242c299 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/message/KafkaMessage.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/message/KafkaMessage.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat import com.fasterxml.jackson.annotation.JsonAlias 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.UtcDateTimes import java.time.LocalDateTime import java.util.* @@ -22,7 +23,7 @@ class KafkaMessage ( @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS[xxx]") @JsonSerialize(using = LocalDateTimeTimeZoneSerializer::class) - val time: LocalDateTime = LocalDateTime.now() + val time: LocalDateTime = UtcDateTimes.now() val dataContentType = "application/json" val specVersion = "1.0" diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/UtcDateTimes.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/UtcDateTimes.kt new file mode 100644 index 0000000..bf2ddcd --- /dev/null +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/UtcDateTimes.kt @@ -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() +} diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/LocalDateTimeTimeZoneSerializer.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/LocalDateTimeTimeZoneSerializer.kt index f8a236e..ebb4fbc 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/LocalDateTimeTimeZoneSerializer.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/LocalDateTimeTimeZoneSerializer.kt @@ -3,20 +3,16 @@ package space.nstart.pcp.pcp_types_lib.utils.json import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.SerializerProvider +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime -import java.time.ZoneId -import java.time.format.DateTimeFormatter class LocalDateTimeTimeZoneSerializer: JsonSerializer() { - companion object { - val fmt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSxxx") - } - override fun serialize(p0: LocalDateTime?, p1: JsonGenerator, p2: SerializerProvider) { - - //todo: проверить корректность зоны по нормальным правилам -// p1.writeString(p0?.atZone(ZoneId.of("Europe/Moscow"))?.format(fmt)) - p1.writeString(p0?.atZone(ZoneId.systemDefault())?.format(fmt)) + if (p0 == null) { + p1.writeNull() + } else { + p1.writeString(UtcDateTimes.format(p0)) + } } } diff --git a/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/utils/UtcDateTimesTest.kt b/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/utils/UtcDateTimesTest.kt new file mode 100644 index 0000000..2e4cc19 --- /dev/null +++ b/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/utils/UtcDateTimesTest.kt @@ -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) + } +} diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionController.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionController.kt index 5deeddd..b915d64 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionController.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionController.kt @@ -8,10 +8,12 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController 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 java.net.URLDecoder import java.nio.charset.StandardCharsets import java.time.LocalDateTime +import java.time.format.DateTimeParseException @RestController @RequestMapping("/api/angular-motion") @@ -43,12 +45,12 @@ class AngularMotionController( return null } val decoded = URLDecoder.decode(raw, StandardCharsets.UTF_8) - val normalized = if (decoded.length == 16) "$decoded:00" else decoded - return runCatching { LocalDateTime.parse(normalized) } - .getOrElse { - throw CustomValidationException( - "Некорректный формат времени '$decoded'. Ожидается yyyy-MM-dd'T'HH:mm или yyyy-MM-dd'T'HH:mm:ss", - ) - } + return try { + UtcDateTimes.parse(decoded) + } catch (_: DateTimeParseException) { + throw CustomValidationException( + "Некорректный формат времени '$decoded'. Ожидается ISO-8601 UTC/offset, например 2026-06-17T06:28:27Z, или legacy UTC-naive yyyy-MM-dd'T'HH:mm:ss", + ) + } } } diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/configuration/TimeConfiguration.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/configuration/TimeConfiguration.kt new file mode 100644 index 0000000..9be1d22 --- /dev/null +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/configuration/TimeConfiguration.kt @@ -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() +} diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/configuration/UtcTimeJsonConfiguration.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/configuration/UtcTimeJsonConfiguration.kt new file mode 100644 index 0000000..361338b --- /dev/null +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/configuration/UtcTimeJsonConfiguration.kt @@ -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::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class UtcLocalDateTimeDeserializer : StdDeserializer(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) + } +} diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/controller/SatelliteController.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/controller/SatelliteController.kt index cf99c62..449a008 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/controller/SatelliteController.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/controller/SatelliteController.kt @@ -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.service.SatelliteIcEventService 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.SatelliteICDTO import java.time.LocalDateTime +import java.time.format.DateTimeParseException @RestController @@ -61,11 +63,11 @@ class SatelliteController { @Parameter(description = "Идентификатор NORAD", example = "56756") @PathVariable("norad_id") noradId : Long, @Parameter(description = "Время начала (если null, то мин. время для выбранного КА)") - @RequestParam("time_start") timeStart : LocalDateTime?, + @RequestParam("time_start", required = false) timeStart : String?, @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( summary = "Получить доступные интервалы прогноза по всем КА", @@ -74,9 +76,9 @@ class SatelliteController { @GetMapping("/orbit/availability") fun getOrbitAvailability( @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}") @@ -96,11 +98,11 @@ class SatelliteController { @Parameter(description = "Идентификатор NORAD", example = "56756") @PathVariable("norad_id") noradId : Long, @Parameter(description = "Время начала (если null, то мин. время для выбранного КА)") - @RequestParam("time_start") timeStart : LocalDateTime?, + @RequestParam("time_start", required = false) timeStart : String?, @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( @@ -112,11 +114,11 @@ class SatelliteController { @Parameter(description = "Идентификатор NORAD", example = "56756") @PathVariable("norad_id") noradId : Long, @Parameter(description = "Время начала (если null, то мин. время для выбранного КА)") - @RequestParam("time_start") timeStart : LocalDateTime?, + @RequestParam("time_start", required = false) timeStart : String?, @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") @PathVariable("norad_id") noradId : Long, @Parameter(description = "Время начала (если null, то мин. время для выбранного КА)") - @RequestParam("time_start") timeStart : LocalDateTime, + @RequestParam("time_start") timeStart : String, @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") @@ -143,4 +145,16 @@ class SatelliteController { ) = 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", + ) + } + } diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/AscNodeEntity.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/AscNodeEntity.kt index a7673b2..943b21e 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/AscNodeEntity.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/AscNodeEntity.kt @@ -1,5 +1,6 @@ package space.nstart.pcp.pcp_request_service.entity +import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes import ballistics.types.KeplerParams import ballistics.types.RevolutionParameter import ballistics.utils.fromDateTime @@ -26,7 +27,7 @@ class AscNodeEntity( @Column(nullable = false) val satelliteId : Long = 0, @Column(name = "t", nullable = false) - val time : LocalDateTime = LocalDateTime.now(), + val time : LocalDateTime = UtcDateTimes.now(), @Column(nullable = false) val revolution : Long = 0, val longitude : Double = 0.0, diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/EarthCellViewsEntity.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/EarthCellViewsEntity.kt index 0e84c33..b1560fb 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/EarthCellViewsEntity.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/EarthCellViewsEntity.kt @@ -1,5 +1,6 @@ package space.nstart.pcp.pcp_request_service.entity +import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes import ballistics.utils.toDateTime import jakarta.persistence.Column import jakarta.persistence.Entity @@ -26,13 +27,13 @@ class EarthCellViewsEntity( @Column(name = "rev_begin", nullable = false) val revolutionBegin : Long = 0, @Column(nullable = false) - val timeBegin : LocalDateTime = LocalDateTime.now(), + val timeBegin : LocalDateTime = UtcDateTimes.now(), @Column(nullable = false) val revSignBegin : Int = 0, @Column(name = "rev_end", nullable = false) val revolutionEnd: Long = 0, @Column(nullable = false) - val timeEnd : LocalDateTime = LocalDateTime.now(), + val timeEnd : LocalDateTime = UtcDateTimes.now(), @Column(nullable = false) val revSignEnd : Int = 0, @Column(name = "roll_min") diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/EarthCoverageEntity.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/EarthCoverageEntity.kt index 2a924d5..b152950 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/EarthCoverageEntity.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/EarthCoverageEntity.kt @@ -1,5 +1,6 @@ package space.nstart.pcp.pcp_request_service.entity +import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes import ballistics.types.FleghtLineSector import ballistics.utils.toDateTime import jakarta.persistence.Column @@ -22,9 +23,9 @@ class EarthCoverageEntity( @Column(nullable = false) val cellNumber : Long = 0, @Column(nullable = false) - val tStart : LocalDateTime = LocalDateTime.now(), + val tStart : LocalDateTime = UtcDateTimes.now(), @Column(nullable = false) - val tStop : LocalDateTime = LocalDateTime.now(), + val tStop : LocalDateTime = UtcDateTimes.now(), ) { constructor(fl : FleghtLineSector, sector : Int, sat : Long) : this( null, diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/FlightLineEntity.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/FlightLineEntity.kt index 7d72c1e..f75edfc 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/FlightLineEntity.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/FlightLineEntity.kt @@ -1,5 +1,6 @@ package space.nstart.pcp.pcp_request_service.entity +import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes import ballistics.types.FlightLine import ballistics.utils.toDateTime import jakarta.persistence.* @@ -19,7 +20,7 @@ class FlightLineEntity( @Column(nullable = false) val satelliteId : Long = 0, @Column(name = "t", nullable = false) - val time : LocalDateTime = LocalDateTime.now(), + val time : LocalDateTime = UtcDateTimes.now(), @Column(nullable = false) val revolution : Long = 0, @Column(nullable = false) diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/PDCMEntity.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/PDCMEntity.kt index afe03da..56fa19b 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/PDCMEntity.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/PDCMEntity.kt @@ -1,5 +1,6 @@ package space.nstart.pcp.pcp_request_service.entity +import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes import ballistics.types.OrbitalPoint import ballistics.utils.fromDateTime import ballistics.utils.math.Vector3D @@ -18,7 +19,7 @@ class PDCMEntity ( @Column(nullable = false) val satelliteId : Long = 0, @Column(name = "t", nullable = false) - val time : LocalDateTime = LocalDateTime.now(), + val time : LocalDateTime = UtcDateTimes.now(), @Column(nullable = false) val revolution : Long = 0, val x: Double = 0.0, diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/SatelliteInitialConditionEntity.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/SatelliteInitialConditionEntity.kt index 044f7de..f0c2a07 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/SatelliteInitialConditionEntity.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/entity/SatelliteInitialConditionEntity.kt @@ -1,5 +1,6 @@ package space.nstart.pcp.pcp_request_service.entity +import space.nstart.pcp.pcp_request_service.utils.UtcDateTimes import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.EnumType @@ -27,7 +28,7 @@ class SatelliteInitialConditionEntity( @Column(name = "movement_model", nullable = false) val movementModel: MovementModel = MovementModel.FOTO, @Column(name = "orb_time", nullable = false) - val orbTime: LocalDateTime = LocalDateTime.now(), + val orbTime: LocalDateTime = UtcDateTimes.now(), @Column(name = "orb_revolution", nullable = false) val orbRevolution: Long = 0, @Column(nullable = false) @@ -47,13 +48,13 @@ class SatelliteInitialConditionEntity( @Column(nullable = false) val f81: Double = 147.8, @Column(name = "received_at", nullable = false) - val receivedAt: LocalDateTime = LocalDateTime.now(), + val receivedAt: LocalDateTime = UtcDateTimes.now(), @Column(name = "calculated_at") var calculatedAt: LocalDateTime? = null, @Column(name = "last_calculated", nullable = false) var lastCalculated: Boolean = false ) { - constructor(request: SatelliteICDTO) : this( + constructor(request: SatelliteICDTO, receivedAt: LocalDateTime = UtcDateTimes.now()) : this( satelliteId = request.satelliteId, movementModel = request.movementModel, orbTime = request.ic.orbPoint.time, @@ -65,10 +66,11 @@ class SatelliteInitialConditionEntity( vy = request.ic.orbPoint.vy, vz = request.ic.orbPoint.vz, 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.lastCalculated = true } diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/service/SatelliteInitialConditionService.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/service/SatelliteInitialConditionService.kt index 8d480e1..2b9d961 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/service/SatelliteInitialConditionService.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/service/SatelliteInitialConditionService.kt @@ -1,6 +1,8 @@ package space.nstart.pcp.pcp_request_service.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 space.nstart.pcp.pcp_request_service.entity.SatelliteInitialConditionEntity import space.nstart.pcp.pcp_request_service.repository.SatelliteInitialConditionRepository @@ -8,17 +10,18 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO @Service class SatelliteInitialConditionService( - private val satelliteInitialConditionRepository: SatelliteInitialConditionRepository + private val satelliteInitialConditionRepository: SatelliteInitialConditionRepository, + private val clock: Clock ) { @Transactional fun saveReceived(request: SatelliteICDTO): SatelliteInitialConditionEntity = - satelliteInitialConditionRepository.save(SatelliteInitialConditionEntity(request)) + satelliteInitialConditionRepository.save(SatelliteInitialConditionEntity(request, UtcDateTimes.now(clock))) @Transactional fun markLastCalculated(initialCondition: SatelliteInitialConditionEntity) { satelliteInitialConditionRepository.clearLastCalculated(initialCondition.satelliteId) - initialCondition.markLastCalculated() + initialCondition.markLastCalculated(UtcDateTimes.now(clock)) satelliteInitialConditionRepository.save(initialCondition) } diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/service/SatelliteService.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/service/SatelliteService.kt index 015437a..9c054ff 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/service/SatelliteService.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/service/SatelliteService.kt @@ -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.PDCMRepository 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.ExactTimePositionRequestDTO 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.TargetPositionDTO import tools.jackson.databind.ObjectMapper +import java.time.Clock import java.time.LocalDateTime import kotlin.collections.forEach import kotlin.math.PI @@ -105,6 +107,8 @@ class SatelliteService { @Autowired private lateinit var stationCatalogClient: StationCatalogClient + @Autowired + private lateinit var clock: Clock val astro = AstronomerJ2000(EarthType.PZ90d02) @@ -761,8 +765,8 @@ class SatelliteService { fun getOrbPoints(id : Long, tn : LocalDateTime?, tk : LocalDateTime?) = pdcmRepository.findBySatelliteIdAndTimeBetween( id, - tn?: LocalDateTime.now(), - tk?: LocalDateTime.now().plusDays(1)) + tn?: UtcDateTimes.now(clock), + tk?: UtcDateTimes.now(clock).plusDays(1)) .map { it.toDTO() } @@ -780,8 +784,8 @@ class SatelliteService { fun getFL(id : Long, tn : LocalDateTime?, tk : LocalDateTime?, step : Double?) = flightLineRepository.findBySatelliteIdAndTimeBetween(id, - tn?: LocalDateTime.now(), - tk?: LocalDateTime.now().plusDays(1) + tn?: UtcDateTimes.now(clock), + tk?: UtcDateTimes.now(clock).plusDays(1) ) .map { it.toDTO() } @@ -902,8 +906,8 @@ class SatelliteService { return ascNodeRepository.findBySatelliteIdAndTimeBetween( id, - tn ?: LocalDateTime.now(), - tk ?: LocalDateTime.now().plusDays(1) + tn ?: UtcDateTimes.now(clock), + tk ?: UtcDateTimes.now(clock).plusDays(1) ) .map { val keps = calculateKeplerParams( diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/utils/UtcDateTimes.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/utils/UtcDateTimes.kt new file mode 100644 index 0000000..89b1888 --- /dev/null +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/utils/UtcDateTimes.kt @@ -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() +} diff --git a/services/pcp-ballistics-service/src/test/kotlin/space/nstart/pcp/pcp_request_service/utils/UtcDateTimesTest.kt b/services/pcp-ballistics-service/src/test/kotlin/space/nstart/pcp/pcp_request_service/utils/UtcDateTimesTest.kt new file mode 100644 index 0000000..30f4f03 --- /dev/null +++ b/services/pcp-ballistics-service/src/test/kotlin/space/nstart/pcp/pcp_request_service/utils/UtcDateTimesTest.kt @@ -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)) + ) + } +} diff --git a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/configuration/UtcTimeJsonConfiguration.kt b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/configuration/UtcTimeJsonConfiguration.kt new file mode 100644 index 0000000..bffb1f6 --- /dev/null +++ b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/configuration/UtcTimeJsonConfiguration.kt @@ -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 { + override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source) +} + +private class UtcLocalDateTimeSerializer : StdSerializer(LocalDateTime::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class UtcLocalDateTimeDeserializer : StdDeserializer(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) + } +} diff --git a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/ComplexPlanRunEntity.kt b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/ComplexPlanRunEntity.kt index 874fb10..d32ecbf 100644 --- a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/ComplexPlanRunEntity.kt +++ b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/ComplexPlanRunEntity.kt @@ -12,6 +12,7 @@ import jakarta.persistence.Id import jakarta.persistence.Index import jakarta.persistence.OneToMany import jakarta.persistence.Table +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @Entity @@ -34,7 +35,7 @@ class ComplexPlanRunEntity( var status: ComplexPlanRunStatus = ComplexPlanRunStatus.CREATED, @Column(name = "created_at", nullable = false) - var createdAt: LocalDateTime = LocalDateTime.now(), + var createdAt: LocalDateTime = UtcDateTimes.now(), @Column(name = "started_at") var startedAt: LocalDateTime? = null, @@ -46,10 +47,10 @@ class ComplexPlanRunEntity( var durationMs: Long? = null, @Column(name = "interval_start", nullable = false) - var intervalStart: LocalDateTime = LocalDateTime.now(), + var intervalStart: LocalDateTime = UtcDateTimes.now(), @Column(name = "interval_end", nullable = false) - var intervalEnd: LocalDateTime = LocalDateTime.now(), + var intervalEnd: LocalDateTime = UtcDateTimes.now(), @Column(name = "requested_by") var requestedBy: String? = null, diff --git a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/SatelliteModeEntity.kt b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/SatelliteModeEntity.kt index a37b4d4..6353438 100644 --- a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/SatelliteModeEntity.kt +++ b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/SatelliteModeEntity.kt @@ -17,6 +17,7 @@ import jakarta.persistence.PreUpdate import jakarta.persistence.Table import jakarta.persistence.Index import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.ModeDataRates +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @Entity @@ -46,10 +47,10 @@ class SatelliteModeEntity( var type: SatelliteModeType = SatelliteModeType.SURVEY, @Column(name = "start_time", nullable = false) - var startTime: LocalDateTime = LocalDateTime.now(), + var startTime: LocalDateTime = UtcDateTimes.now(), @Column(name = "end_time", nullable = false) - var endTime: LocalDateTime = LocalDateTime.now(), + var endTime: LocalDateTime = UtcDateTimes.now(), @Column(name = "revolution", nullable = false) var revolution: Long = 0, diff --git a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/SatelliteModeSnapshotEntity.kt b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/SatelliteModeSnapshotEntity.kt index 4d44d0e..e375e1a 100644 --- a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/SatelliteModeSnapshotEntity.kt +++ b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/SatelliteModeSnapshotEntity.kt @@ -12,6 +12,7 @@ import jakarta.persistence.JoinColumn import jakarta.persistence.ManyToOne import jakarta.persistence.OneToMany import jakarta.persistence.Table +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @Entity @@ -34,13 +35,13 @@ class SatelliteModeSnapshotEntity( var satelliteId: Long = 0, @Column(name = "interval_start", nullable = false) - var intervalStart: LocalDateTime = LocalDateTime.now(), + var intervalStart: LocalDateTime = UtcDateTimes.now(), @Column(name = "interval_end", nullable = false) - var intervalEnd: LocalDateTime = LocalDateTime.now(), + var intervalEnd: LocalDateTime = UtcDateTimes.now(), @Column(name = "created_at", nullable = false) - var createdAt: LocalDateTime = LocalDateTime.now(), + var createdAt: LocalDateTime = UtcDateTimes.now(), @Column(name = "is_active", nullable = false) var isActive: Boolean = false, diff --git a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/model/mode/SurveyMode.kt b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/model/mode/SurveyMode.kt index 05fc0bf..32ac02c 100644 --- a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/model/mode/SurveyMode.kt +++ b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/model/mode/SurveyMode.kt @@ -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_types_lib.dto.satellite.modes.SurveyModeInfoDTO +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class SurveyMode( val revolution : Long = 1, - val time : LocalDateTime = LocalDateTime.now(), - val timeStop : LocalDateTime = LocalDateTime.now(), + val time : LocalDateTime = UtcDateTimes.now(), + val timeStop : LocalDateTime = UtcDateTimes.now(), val roll : Double = 0.0, val latitude : Double = 0.0, val longitude : Double = 0.0, diff --git a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/service/SlotService.kt b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/service/SlotService.kt index 1b04b76..1ad1cd4 100644 --- a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/service/SlotService.kt +++ b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/service/SlotService.kt @@ -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.SurveySlotDTO import tools.jackson.databind.ObjectMapper +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @Service @@ -34,8 +35,8 @@ class SlotService(webClientBuilderProvider: ObjectProvider) { UriComponentsBuilder .fromUriString("/api/slots/poly-cover") .queryParam("wkt", wkt) - .queryParam("timeStart", tn) - .queryParam("timeStop", tk) + .queryParam("timeStart", UtcDateTimes.format(tn)) + .queryParam("timeStop", UtcDateTimes.format(tk)) .queryParam("cov", cov) .apply { sats.forEach { queryParam("satellites", it) } @@ -49,8 +50,8 @@ class SlotService(webClientBuilderProvider: ObjectProvider) { .uri( "/api/slots/poly-cover?wkt={wkt}&timeStart={timeStart}&timeStop={timeStop}&revSign={revSign}&cov={cov}", wkt, - tn, - tk, + UtcDateTimes.format(tn), + UtcDateTimes.format(tk), sign, cov ) @@ -71,8 +72,8 @@ class SlotService(webClientBuilderProvider: ObjectProvider) { .uri( "/api/slots/mission/satellite?satellite_id={satelliteId}&timeStart={timeStart}&timeStop={timeStop}", id, - tn, - tk + UtcDateTimes.format(tn), + UtcDateTimes.format(tk) ) .retrieve() .onStatus({ status -> status.isError }) { response -> diff --git a/services/pcp-coverage-scheme-service/src/main/kotlin/space/nstart/pcp/pcp_coverage_scheme_service/client/BallisticsClient.kt b/services/pcp-coverage-scheme-service/src/main/kotlin/space/nstart/pcp/pcp_coverage_scheme_service/client/BallisticsClient.kt index 201d82a..6b86439 100644 --- a/services/pcp-coverage-scheme-service/src/main/kotlin/space/nstart/pcp/pcp_coverage_scheme_service/client/BallisticsClient.kt +++ b/services/pcp-coverage-scheme-service/src/main/kotlin/space/nstart/pcp/pcp_coverage_scheme_service/client/BallisticsClient.kt @@ -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.PointViewParamDTO 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.LocalDateTime @@ -82,8 +83,8 @@ class BallisticsClient( .uri { uriBuilder -> uriBuilder .path("/api/satellites/{satelliteId}/flight-line") - .queryParam("time_start", timeStart) - .queryParam("time_stop", timeStop) + .queryParam("time_start", UtcDateTimes.format(timeStart)) + .queryParam("time_stop", UtcDateTimes.format(timeStop)) .build(satelliteId) } .retrieve() diff --git a/services/pcp-coverage-scheme-service/src/main/kotlin/space/nstart/pcp/pcp_coverage_scheme_service/configuration/UtcTimeConfiguration.kt b/services/pcp-coverage-scheme-service/src/main/kotlin/space/nstart/pcp/pcp_coverage_scheme_service/configuration/UtcTimeConfiguration.kt new file mode 100644 index 0000000..b1e9dd5 --- /dev/null +++ b/services/pcp-coverage-scheme-service/src/main/kotlin/space/nstart/pcp/pcp_coverage_scheme_service/configuration/UtcTimeConfiguration.kt @@ -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 { + override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source) +} + +private class UtcLocalDateTimeSerializer : StdSerializer(LocalDateTime::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class UtcLocalDateTimeDeserializer : StdDeserializer(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) + } +} diff --git a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/JacksonConfig.kt b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/JacksonConfig.kt index 6c33e87..ce200a8 100644 --- a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/JacksonConfig.kt +++ b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/JacksonConfig.kt @@ -1,11 +1,21 @@ 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.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.module.kotlin.registerKotlinModule 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 JacksonConfig { @@ -13,8 +23,26 @@ class JacksonConfig { fun objectMapper(): ObjectMapper { val mapper = ObjectMapper() mapper.registerModule(JavaTimeModule()) + .registerModule(utcLocalDateTimeModule()) .registerKotlinModule() .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 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() { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class Jackson2UtcLocalDateTimeDeserializer : JsonDeserializer() { + 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) + } +} diff --git a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/apl/BasicPlanCalculator.kt b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/apl/BasicPlanCalculator.kt index be20de9..c529ed5 100644 --- a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/apl/BasicPlanCalculator.kt +++ b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/apl/BasicPlanCalculator.kt @@ -58,6 +58,7 @@ import kotlin.math.abs import kotlin.math.max import kotlin.math.roundToLong 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_ROUTE_WIDTH_RATIO = 0.9 @@ -841,7 +842,7 @@ class BasicPlanCalculator( val resultFilePath = if (request.saveResultToFile && status == ComplexPlanCalculationStatus.COMPLETED) { observationParametersDebugFileWriter.write( ObservationParametersDebugFileDTO( - createdAt = LocalDateTime.now(), + createdAt = UtcDateTimes.now(), requestId = request.requestId, satelliteIds = selectedSatelliteIds, missingSatelliteIds = missingSatelliteIds, diff --git a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/config/UtcTimeConfiguration.kt b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/config/UtcTimeConfiguration.kt new file mode 100644 index 0000000..331e97d --- /dev/null +++ b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/config/UtcTimeConfiguration.kt @@ -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 { + override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source) +} + +private class UtcLocalDateTimeSerializer : StdSerializer(LocalDateTime::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class UtcLocalDateTimeDeserializer : StdDeserializer(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) + } +} diff --git a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/db/entity/DynamicPlanRouteEntity.kt b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/db/entity/DynamicPlanRouteEntity.kt index 259202f..594b7aa 100644 --- a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/db/entity/DynamicPlanRouteEntity.kt +++ b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/db/entity/DynamicPlanRouteEntity.kt @@ -11,6 +11,7 @@ import jakarta.persistence.ManyToOne import jakarta.persistence.Table import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes @Table(name = "dynamic_plan_route") @Entity @@ -31,10 +32,10 @@ class DynamicPlanRouteEntity( var satelliteId: Long = 0, @Column(name = "start_time", nullable = false) - var startTime: LocalDateTime = LocalDateTime.now(), + var startTime: LocalDateTime = UtcDateTimes.now(), @Column(name = "end_time", nullable = false) - var endTime: LocalDateTime = LocalDateTime.now(), + var endTime: LocalDateTime = UtcDateTimes.now(), @Column(name = "duration", nullable = false) var duration: Double = 0.0, diff --git a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/db/entity/DynamicPlanRunEntity.kt b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/db/entity/DynamicPlanRunEntity.kt index a307bdb..837c99a 100644 --- a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/db/entity/DynamicPlanRunEntity.kt +++ b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/db/entity/DynamicPlanRunEntity.kt @@ -11,6 +11,7 @@ import space.nstart.pcp.complan.types.DynamicPlanRevolutionMode import space.nstart.pcp.complan.types.DynamicPlanRunStatus import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes @Table(name = "dynamic_plan_run") @Entity @@ -47,7 +48,7 @@ class DynamicPlanRunEntity( var filterCoveredRoutes: Boolean = false, @Column(name = "created_at", nullable = false) - var createdAt: LocalDateTime = LocalDateTime.now(), + var createdAt: LocalDateTime = UtcDateTimes.now(), @Column(name = "started_at") var startedAt: LocalDateTime? = null, diff --git a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/BallTaskServce.kt b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/BallTaskServce.kt index 7fd96b8..8c59847 100644 --- a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/BallTaskServce.kt +++ b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/BallTaskServce.kt @@ -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.FlightLineDTO 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.SatelliteOrbitAvailabilityDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime /**Получение данных от сервиса баллистики*/ @@ -39,7 +39,7 @@ class BallTaskServce { UriComponentsBuilder .fromUriString(ballisticsServiceUrl) .path("/api/satellites/orbit/availability") - .apply { time?.let { queryParam("time", it) } } + .apply { time?.let { queryParam("time", UtcDateTimes.format(it)) } } .toUriString() ) .retrieve() @@ -53,8 +53,8 @@ class BallTaskServce { UriComponentsBuilder .fromUriString(ballisticsServiceUrl) .path("/api/satellites/$noradId/flight-line") - .queryParam("time_start", timeStart) - .queryParam("time_stop", timeStop) + .queryParam("time_start", UtcDateTimes.format(timeStart)) + .queryParam("time_stop", UtcDateTimes.format(timeStop)) .toUriString() ) .retrieve() @@ -65,7 +65,13 @@ class BallTaskServce { RestClient.create() .post() .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() .body(ORB_POINT_LIST_TYPE) .orEmpty() @@ -80,16 +86,16 @@ class BallTaskServce { .post() .uri(buildBallisticsUri("/api/obj-view/mpl-square")) .body( - BallisticsObjViewRequestDTO( - satellites = satellites, - timeStart = timeStart, - timeStop = timeStop, - obj = BallisticsObjDTO( + mapOf( + "satellites" to satellites, + "timeStart" to UtcDateTimes.format(timeStart), + "timeStop" to UtcDateTimes.format(timeStop), + "obj" to BallisticsObjDTO( id = obj.id.toString(), position = obj.position, contourWKT = obj.contourWKT ), - sunAngleMin = objects.sunAngleMin + "sunAngleMin" to objects.sunAngleMin ) ) .retrieve() diff --git a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/DynamicPlanRunService.kt b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/DynamicPlanRunService.kt index 3afa0da..b2c47c8 100644 --- a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/DynamicPlanRunService.kt +++ b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/DynamicPlanRunService.kt @@ -28,6 +28,7 @@ import java.time.LocalDateTime import java.util.UUID import java.util.concurrent.ExecutorService import kotlin.math.roundToLong +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes @Service class DynamicPlanRunService( @@ -59,7 +60,7 @@ class DynamicPlanRunService( calculationMode = request.calculationMode, revolutionMode = request.revolutionMode, filterCoveredRoutes = request.filterCoveredRoutes, - createdAt = LocalDateTime.now() + createdAt = UtcDateTimes.now() ) ) } ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create calculation run") @@ -135,7 +136,7 @@ class DynamicPlanRunService( return@execute 0 } - val finishedAt = LocalDateTime.now() + val finishedAt = UtcDateTimes.now() runs.forEach { run -> run.status = DynamicPlanRunStatus.FAILED run.finishedAt = finishedAt @@ -171,7 +172,7 @@ class DynamicPlanRunService( transactionTemplate.execute { val run = runRepository.findById(runId).orElse(null) ?: return@execute null run.status = DynamicPlanRunStatus.RUNNING - run.startedAt = LocalDateTime.now() + run.startedAt = UtcDateTimes.now() run.errorMessage = null runRepository.save(run) run.toRequest() @@ -182,7 +183,7 @@ class DynamicPlanRunService( val run = runRepository.findById(runId).orElseThrow() val result = output.result run.status = DynamicPlanRunStatus.COMPLETED - run.finishedAt = LocalDateTime.now() + run.finishedAt = UtcDateTimes.now() run.durationMs = result.durationMs run.resultStatus = result.status.name run.routesCount = result.routesCount @@ -200,7 +201,7 @@ class DynamicPlanRunService( transactionTemplate.execute { val run = runRepository.findById(runId).orElseThrow() run.status = DynamicPlanRunStatus.FAILED - run.finishedAt = LocalDateTime.now() + run.finishedAt = UtcDateTimes.now() run.errorMessage = error.message ?: error::class.java.simpleName runRepository.save(run) } diff --git a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/spacecraft/SpaceCraft.kt b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/spacecraft/SpaceCraft.kt index 853aeec..1701b15 100755 --- a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/spacecraft/SpaceCraft.kt +++ b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/spacecraft/SpaceCraft.kt @@ -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.RevolutionSign import java.time.LocalDateTime +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Класс космического аппарата** @@ -17,9 +18,9 @@ class SpaceCraft ( val flightLine : MutableList = mutableListOf() /**Время начала планирования аппарата*/ - var timeStart: LocalDateTime = LocalDateTime.now() + var timeStart: LocalDateTime = UtcDateTimes.now() /**Время конца планирования аппарата*/ - var timeStop : LocalDateTime = LocalDateTime.now() + var timeStop : LocalDateTime = UtcDateTimes.now() /** * Создание линий полета восходящего и нисходящего витка * @param geomFactory фабрика геометрии diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/configuration/Jackson2UtcConfiguration.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/configuration/Jackson2UtcConfiguration.kt new file mode 100644 index 0000000..e33b89e --- /dev/null +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/configuration/Jackson2UtcConfiguration.kt @@ -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() { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class Jackson2UtcLocalDateTimeDeserializer : JsonDeserializer() { + 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) + } +} diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/configuration/UtcTimeConfiguration.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/configuration/UtcTimeConfiguration.kt new file mode 100644 index 0000000..db547b1 --- /dev/null +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/configuration/UtcTimeConfiguration.kt @@ -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 { + override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source) +} + +private class UtcLocalDateTimeSerializer : StdSerializer(LocalDateTime::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class UtcLocalDateTimeDeserializer : StdDeserializer(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) + } +} diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/BallisticsService.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/BallisticsService.kt index e991305..e519113 100644 --- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/BallisticsService.kt +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/BallisticsService.kt @@ -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.SquareViewParamDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamsWithPointsDTO +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @@ -55,8 +56,8 @@ class BallisticsService(webClientBuilderProvider: ObjectProvider() .toIterable() diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/BookedSlotsEventPublisher.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/BookedSlotsEventPublisher.kt index 4587cc3..83f34cf 100644 --- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/BookedSlotsEventPublisher.kt +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/BookedSlotsEventPublisher.kt @@ -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.message.KafkaMessage 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.time.LocalDateTime +import java.time.Clock import java.util.UUID @Component @@ -26,7 +27,8 @@ class BookedSlotsEventPublisher( kafkaTemplateProvider: ObjectProvider>, objectMapperProvider: ObjectProvider, @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) @@ -59,7 +61,7 @@ class BookedSlotsEventPublisher( val event = BookedSlotsStatusChangedEvent( bookedSlotIds = normalizedIds, status = status, - occurredAt = LocalDateTime.now(), + occurredAt = UtcDateTimes.now(clock), sourceService = applicationName, missionId = missionId, modeId = modeId, diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/ComplexMissionService.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/ComplexMissionService.kt index 69d31cb..6788f9e 100644 --- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/ComplexMissionService.kt +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/ComplexMissionService.kt @@ -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.dto.satellite.mission.SatelliteModeResponseDTO 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 java.time.LocalDateTime @@ -70,8 +71,8 @@ class ComplexMissionService(webClientBuilderProvider: ObjectProvider status.isError }) { response -> diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt index 12df33a..2ec7e14 100644 --- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt @@ -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.requests.slots.BookedSlotStatus 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.LocalDateTime import java.util.UUID @@ -81,6 +83,7 @@ class MissionPlaningService( private val complexMissionService: ComplexMissionService, private val satelliteCatalogClient: SatelliteCatalogClient, private val satelliteMissionTypeFactory: SatelliteMissionTypeFactory, + private val clock: Clock = Clock.systemUTC(), private val logger: Logger = LoggerFactory.getLogger(MissionPlaningService::class.java) ) { @@ -432,7 +435,7 @@ class MissionPlaningService( overrideIntervalEnd: LocalDateTime? = null, comPlanSnapshotId: Long? = null ): MissionSurveyCalculationResponseDTO { - val startedAt = LocalDateTime.now() + val startedAt = UtcDateTimes.now(clock) logger.info( "Расчет плана съемки stage 5 по persisted snapshot этапа 4 для миссии {}: requestedComPlanSnapshotId={}", missionId, @@ -520,7 +523,7 @@ class MissionPlaningService( ) if (survs.isEmpty()) { - val finishedAt = LocalDateTime.now() + val finishedAt = UtcDateTimes.now(clock) return MissionSurveyCalculationResponseDTO( missionId = missionId, completed = true, @@ -565,7 +568,7 @@ class MissionPlaningService( modeBookings.size ) - val finishedAt = LocalDateTime.now() + val finishedAt = UtcDateTimes.now(clock) return MissionSurveyCalculationResponseDTO( missionId = missionId, completed = true, diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/config/JacksonConfig.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/config/JacksonConfig.kt index 3d796ec..1cbfe26 100644 --- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/config/JacksonConfig.kt +++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/config/JacksonConfig.kt @@ -1,10 +1,19 @@ 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.SerializerProvider +import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule 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 JacksonConfig { @@ -18,5 +27,23 @@ class JacksonConfig { return jacksonObjectMapper() .registerModule(JavaTimeModule()) .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() { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class Jackson2UtcLocalDateTimeDeserializer : JsonDeserializer() { + 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) } } diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/config/UtcTimeConfiguration.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/config/UtcTimeConfiguration.kt new file mode 100644 index 0000000..cb1c225 --- /dev/null +++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/config/UtcTimeConfiguration.kt @@ -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 { + override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source) +} + +private class UtcLocalDateTimeSerializer : StdSerializer(LocalDateTime::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class UtcLocalDateTimeDeserializer : StdDeserializer(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) + } +} diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/controller/RequestController.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/controller/RequestController.kt index dd56310..f4d08f1 100644 --- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/controller/RequestController.kt +++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/controller/RequestController.kt @@ -12,7 +12,6 @@ import org.nstart.dep265.requestservice.dto.RequestStatusDto import org.nstart.dep265.requestservice.dto.RequestWithCellsResponseDto import org.nstart.dep265.requestservice.dto.SurveyTypeDto import org.nstart.dep265.requestservice.service.RequestService -import org.springframework.format.annotation.DateTimeFormat import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping @@ -41,10 +40,10 @@ class RequestController( @RequestParam(required = false) surveyType: SurveyTypeDto?, @RequestParam(required = false) kpp: Int?, @RequestParam(required = false) highPriorityTransmit: Boolean?, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginFrom: LocalDateTime?, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginTo: LocalDateTime?, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endFrom: LocalDateTime?, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endTo: LocalDateTime?, + @RequestParam(required = false) beginFrom: LocalDateTime?, + @RequestParam(required = false) beginTo: LocalDateTime?, + @RequestParam(required = false) endFrom: LocalDateTime?, + @RequestParam(required = false) endTo: LocalDateTime?, @RequestParam(defaultValue = "false") includeDeleted: Boolean, @RequestParam(defaultValue = "0") page: Int, @RequestParam(defaultValue = "50") size: Int, @@ -73,10 +72,10 @@ class RequestController( @RequestParam(required = false) surveyType: SurveyTypeDto?, @RequestParam(required = false) kpp: Int?, @RequestParam(required = false) highPriorityTransmit: Boolean?, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginFrom: LocalDateTime?, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginTo: LocalDateTime?, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endFrom: LocalDateTime?, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endTo: LocalDateTime?, + @RequestParam(required = false) beginFrom: LocalDateTime?, + @RequestParam(required = false) beginTo: LocalDateTime?, + @RequestParam(required = false) endFrom: LocalDateTime?, + @RequestParam(required = false) endTo: LocalDateTime?, @RequestParam(defaultValue = "false") includeDeleted: Boolean, @RequestParam(defaultValue = "0") page: Int, @RequestParam(defaultValue = "50") size: Int, diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/GridRebuildService.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/GridRebuildService.kt index 39d0438..aac9016 100644 --- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/GridRebuildService.kt +++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/GridRebuildService.kt @@ -7,6 +7,8 @@ import org.nstart.dep265.requestservice.repository.RequestCellRepository import org.nstart.dep265.requestservice.repository.RequestRepository import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes +import java.time.Clock import java.time.LocalDateTime class GridRebuildAlreadyRunningException : RuntimeException("Grid rebuild is already running") @@ -20,6 +22,7 @@ class GridRebuildService( private val requestGridProjectionService: RequestGridProjectionService, private val earthCellRepository: EarthCellRepository, private val gridRebuildLockService: GridRebuildLockService, + private val clock: Clock = Clock.systemUTC(), ) { @Transactional fun rebuildGrid(): GridRebuildResponseDto { @@ -40,7 +43,7 @@ class GridRebuildService( return GridRebuildResponseDto( cellsCount = earthCellRepository.count(), rebuiltRequestProjectionsCount = activeRequests.size.toLong(), - rebuiltAt = LocalDateTime.now(), + rebuiltAt = UtcDateTimes.now(clock), ) } diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/OutboxPublisherService.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/OutboxPublisherService.kt index 7077639..8433f0d 100644 --- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/OutboxPublisherService.kt +++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/OutboxPublisherService.kt @@ -11,6 +11,8 @@ import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Service import org.springframework.transaction.support.TransactionCallback 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.LocalDateTime import java.util.concurrent.ExecutionException @@ -25,6 +27,7 @@ class OutboxPublisherService( @param:Qualifier("outboxTransactionOperations") private val transactionOperations: TransactionOperations, + private val clock: Clock = Clock.systemUTC(), ) { private val log = LoggerFactory.getLogger(this::class.java) @@ -42,7 +45,7 @@ class OutboxPublisherService( return try { transactionOperations.execute( TransactionCallback { - val now = LocalDateTime.now() + val now = UtcDateTimes.now(clock) val events = outboxEventRepository.findPublishableForUpdateSkipLocked( eventType = RequestStatusOutboxService.REQUEST_STATUS_EVENT_TYPE, now = now, diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestExpiryService.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestExpiryService.kt index fd73d08..a86c99b 100644 --- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestExpiryService.kt +++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestExpiryService.kt @@ -9,6 +9,8 @@ import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Service import org.springframework.transaction.support.TransactionCallback import org.springframework.transaction.support.TransactionOperations +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes +import java.time.Clock import java.time.LocalDateTime /** @@ -33,6 +35,7 @@ class RequestExpiryService( @param:Qualifier("requestExpiryTransactionOperations") private val transactionOperations: TransactionOperations, + private val clock: Clock = Clock.systemUTC(), ) { private val log = LoggerFactory.getLogger(this::class.java) @@ -55,7 +58,7 @@ class RequestExpiryService( return 0 } val maxBatches = requestExpiryProperties.maxBatchesPerRun.coerceAtLeast(1) - val now = LocalDateTime.now() + val now = UtcDateTimes.now(clock) var expiredTotal = 0 var batchIndex = 0 diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt index cee9cbc..461dbb2 100644 --- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt +++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt @@ -38,6 +38,8 @@ import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Sort import org.springframework.stereotype.Service 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.util.UUID @@ -53,6 +55,7 @@ class RequestService( private val requestGridProjectionService: RequestGridProjectionService, private val requestLifecycleService: RequestLifecycleService, private val entityManager: EntityManager, + private val clock: Clock = Clock.systemUTC(), ) { @Transactional fun createRequest(request: CreateRequestDto): CreateRequestResponseDto { @@ -65,7 +68,7 @@ class RequestService( "beginDateTime должен быть раньше endDateTime" } - val now = LocalDateTime.now() + val now = UtcDateTimes.now(clock) val normalizedGeometry = geometryService.parsePolygonalGeometry(request.geometry) val normalizedGeometryWkt = geometryService.toWkt(normalizedGeometry) val geometryArea = geometryService.area(normalizedGeometry) @@ -159,7 +162,7 @@ class RequestService( @Transactional fun deleteRequest(id: UUID): DeleteRequestResponseDto? { 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.deletedAt = deletedAt request.updatedAt = deletedAt diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RouteMatchingService.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RouteMatchingService.kt index 5e5934c..64a7cf4 100644 --- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RouteMatchingService.kt +++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RouteMatchingService.kt @@ -14,6 +14,8 @@ import org.springframework.dao.DuplicateKeyException import org.springframework.stereotype.Service import org.springframework.transaction.support.TransactionCallback 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.util.UUID import kotlin.math.max @@ -35,6 +37,7 @@ class RouteMatchingService( @param:Qualifier("routeMatchingTransactionOperations") private val routeMatchingTransactionOperations: TransactionOperations, + private val clock: Clock = Clock.systemUTC(), ) { private val log = LoggerFactory.getLogger(this::class.java) @@ -43,7 +46,7 @@ class RouteMatchingService( val routeId = route.routeId val routeRollRange = route.rollAngle - val matchedAt: LocalDateTime = LocalDateTime.now() + val matchedAt: LocalDateTime = UtcDateTimes.now(clock) val routeGeometry = geometryService.parsePolygonalGeometry(route.geometry) val candidateRequestIds = requestRepository.findRequestIdsForRoute( diff --git a/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/config/UtcTimeConfiguration.kt b/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/config/UtcTimeConfiguration.kt new file mode 100644 index 0000000..26a75f5 --- /dev/null +++ b/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/config/UtcTimeConfiguration.kt @@ -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 { + override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source) +} + +private class UtcLocalDateTimeSerializer : StdSerializer(LocalDateTime::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class UtcLocalDateTimeDeserializer : StdDeserializer(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) + } +} diff --git a/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/service/RoutePassportIngestionService.kt b/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/service/RoutePassportIngestionService.kt index fe4d09e..4c5e280 100644 --- a/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/service/RoutePassportIngestionService.kt +++ b/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/service/RoutePassportIngestionService.kt @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.ObjectMapper as JpaJsonObjectMapper import tools.jackson.databind.ObjectMapper import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Сохраняет разобранный route passport и outbox-события в одной DB transaction. @@ -38,7 +39,7 @@ class RoutePassportIngestionService( rawPayload: String, sourceRecord: RoutePassportSourceRecordMetadata, ) { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val routePassportDto = parsedMessage.routePassportDto val existingRoutePassport = routePassportRepository.findById(routePassportDto.routeId) val routeCreated = existingRoutePassport.isEmpty diff --git a/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/service/RoutePassportJsonParserService.kt b/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/service/RoutePassportJsonParserService.kt index 208d351..2667305 100644 --- a/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/service/RoutePassportJsonParserService.kt +++ b/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/service/RoutePassportJsonParserService.kt @@ -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.RoutePassportMessageMetadata import space.nstart.pcp.routepassportconsumer.exception.RoutePassportContractException +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import tools.jackson.core.JacksonException import tools.jackson.databind.ObjectMapper import java.math.BigDecimal import java.nio.charset.StandardCharsets -import java.time.LocalDateTime import java.util.UUID /** @@ -63,8 +63,8 @@ class RoutePassportJsonParserService( routeNameShort = collection, orbitNumber = properties.orbitNumber.toLong(), orbitState = properties.orbitState, - intervalBegin = LocalDateTime.parse(properties.intervalBegin), - intervalEnd = LocalDateTime.parse(properties.intervalEnd), + intervalBegin = UtcDateTimes.parse(properties.intervalBegin), + intervalEnd = UtcDateTimes.parse(properties.intervalEnd), rollAngle = properties.rollAngle.toAngleRangeDto(fieldName = "rollAngle"), visirAngle = properties.visirAngle.toAngleRangeDto(fieldName = "visirAngle"), resolutionRange = properties.resolutionRange.toBigDecimal(), diff --git a/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/service/RoutePassportOutboxService.kt b/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/service/RoutePassportOutboxService.kt index 052c820..8f4998c 100644 --- a/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/service/RoutePassportOutboxService.kt +++ b/services/pcp-route-processing-service/src/main/kotlin/space/nstart/pcp/routepassportconsumer/service/RoutePassportOutboxService.kt @@ -10,6 +10,7 @@ import space.nstart.pcp.routepassportconsumer.repository.RoutePassportOutboxRepo import java.time.Duration import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Owns transactional claim and status transitions for route passport outbox rows. @@ -32,7 +33,7 @@ class RoutePassportOutboxService( return emptyList() } - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val lockedUntil = now.plus(Duration.ofMillis(outboxProperties.lockTimeoutMs)) val claimedRows = routePassportOutboxRepository.findDueForUpdateSkipLocked(outboxProperties.batchSize) @@ -70,7 +71,7 @@ class RoutePassportOutboxService( */ @Transactional fun markPublished(outboxId: UUID) { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val outbox = findOutbox(outboxId) outbox.status = RoutePassportOutboxStatus.PUBLISHED @@ -99,7 +100,7 @@ class RoutePassportOutboxService( */ @Transactional fun markRetryOrFailed(outboxId: UUID, exception: Exception) { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val outbox = findOutbox(outboxId) outbox.attempts += 1 diff --git a/services/pcp-satellite-catalog-service/src/main/kotlin/space/nstart/pcp/pcp_satellite_catalog_service/configuration/UtcTimeConfiguration.kt b/services/pcp-satellite-catalog-service/src/main/kotlin/space/nstart/pcp/pcp_satellite_catalog_service/configuration/UtcTimeConfiguration.kt new file mode 100644 index 0000000..9d3d188 --- /dev/null +++ b/services/pcp-satellite-catalog-service/src/main/kotlin/space/nstart/pcp/pcp_satellite_catalog_service/configuration/UtcTimeConfiguration.kt @@ -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 { + override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source) +} + +private class UtcLocalDateTimeSerializer : StdSerializer(LocalDateTime::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class UtcLocalDateTimeDeserializer : StdDeserializer(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) + } +} diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/CamundaJacksonConfig.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/CamundaJacksonConfig.kt index ecc1537..fc9a419 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/CamundaJacksonConfig.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/CamundaJacksonConfig.kt @@ -1,15 +1,24 @@ package space.nstart.pcp_tgu_service.config /** Настраивает Jackson-сериализацию для payload'ов клиента Camunda. */ +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.KotlinModule import io.camunda.client.api.JsonMapper as CamundaJsonMapper import io.camunda.client.impl.CamundaObjectMapper import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes +import java.time.LocalDateTime @Configuration class CamundaJacksonConfig { @@ -24,6 +33,7 @@ class CamundaJacksonConfig { return JsonMapper.builder() .addModule(KotlinModule.Builder().build()) .addModule(JavaTimeModule()) + .addModule(jackson2UtcLocalDateTimeModule()) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .build() } @@ -34,3 +44,21 @@ class CamundaJacksonConfig { return CamundaObjectMapper(objectMapper) } } + + +private fun jackson2UtcLocalDateTimeModule(): SimpleModule = SimpleModule("pcp-tgu-service-jackson2-utc-local-date-time") + .addSerializer(LocalDateTime::class.java, Jackson2UtcLocalDateTimeSerializer()) + .addDeserializer(LocalDateTime::class.java, Jackson2UtcLocalDateTimeDeserializer()) + +private class Jackson2UtcLocalDateTimeSerializer : JsonSerializer() { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class Jackson2UtcLocalDateTimeDeserializer : JsonDeserializer() { + 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) + } +} diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/UtcTimeConfiguration.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/UtcTimeConfiguration.kt new file mode 100644 index 0000000..6b85015 --- /dev/null +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/UtcTimeConfiguration.kt @@ -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 { + override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source) +} + +private class UtcLocalDateTimeSerializer : StdSerializer(LocalDateTime::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class UtcLocalDateTimeDeserializer : StdDeserializer(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) + } +} diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/TestTguController.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/TestTguController.kt index 1999530..91e955e 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/TestTguController.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/TestTguController.kt @@ -1,7 +1,6 @@ package space.nstart.pcp_tgu_service.controller import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty -import org.springframework.format.annotation.DateTimeFormat import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -90,12 +89,8 @@ class TestTguController( @GetMapping("/external-points/{noradId}") fun getExternalPoints( @PathVariable noradId: Long, - @RequestParam("time_start") - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) - timeStart: LocalDateTime, - @RequestParam("time_stop") - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) - timeStop: LocalDateTime + @RequestParam("time_start") timeStart: LocalDateTime, + @RequestParam("time_stop") timeStop: LocalDateTime ): String = externalPointsClient.fetchRawVisibilityPayload(noradId, timeStart, timeStop) diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponse.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponse.kt index 4aebbef..911a7c2 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponse.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponse.kt @@ -10,8 +10,8 @@ import java.util.UUID /** * DTO ответа REST API для плана. * - * Времена ([startTime]/[endTime]) — naive `LocalDateTime`, сериализуются как ISO-строка без зоны - * (напр. `"2026-05-29T01:00:00"`). Хранение, домен и API работают на одних настенных часах. + * Времена ([startTime]/[endTime]) — UTC-naive `LocalDateTime` в домене/БД. + * На REST-границе сериализуются как ISO-8601 UTC с `Z` (например `"2026-05-29T01:00:00Z"`). */ data class PlanResponse( @@ -21,10 +21,10 @@ data class PlanResponse( /** Идентификатор космического аппарата */ val spacecraftId: String, - /** Время начала плана (ISO без зоны). */ + /** Время начала плана (UTC). */ val startTime: LocalDateTime, - /** Время окончания плана (ISO без зоны). */ + /** Время окончания плана (UTC). */ val endTime: LocalDateTime, /** Идентификатор пункта закладки */ diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/AcceptedPlanSlotsUpdateJobEntity.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/AcceptedPlanSlotsUpdateJobEntity.kt index 66d1cf5..06f9f4b 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/AcceptedPlanSlotsUpdateJobEntity.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/AcceptedPlanSlotsUpdateJobEntity.kt @@ -8,6 +8,7 @@ import jakarta.persistence.Id import jakarta.persistence.Table import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Persisted job на запуск короткого BPMN-процесса обновления слотов accepted plan. @@ -39,10 +40,10 @@ class AcceptedPlanSlotsUpdateJobEntity( var lastError: String? = null, @Column(name = "created_at", nullable = false) - var createdAt: LocalDateTime = LocalDateTime.now(), + var createdAt: LocalDateTime = UtcDateTimes.now(), @Column(name = "updated_at", nullable = false) - var updatedAt: LocalDateTime = LocalDateTime.now() + var updatedAt: LocalDateTime = UtcDateTimes.now() ) { /** Создаёт пустой экземпляр сущности, необходимый для JPA. */ constructor() : this(planId = UUID.randomUUID()) diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/PlanIssueAttemptEntity.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/PlanIssueAttemptEntity.kt index 26cea9b..4475396 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/PlanIssueAttemptEntity.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/PlanIssueAttemptEntity.kt @@ -8,6 +8,7 @@ import jakarta.persistence.Id import jakarta.persistence.Table import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Одна попытка выдачи persisted plan через один BPMN process instance. @@ -45,10 +46,10 @@ class PlanIssueAttemptEntity( var finishedAt: LocalDateTime? = null, @Column(name = "created_at", nullable = false) - var createdAt: LocalDateTime = LocalDateTime.now(), + var createdAt: LocalDateTime = UtcDateTimes.now(), @Column(name = "updated_at", nullable = false) - var updatedAt: LocalDateTime = LocalDateTime.now() + var updatedAt: LocalDateTime = UtcDateTimes.now() ) { /** Создаёт пустой экземпляр сущности, необходимый для JPA. */ constructor() : this(attemptId = UUID.randomUUID(), planId = UUID.randomUUID(), attemptNo = 0) diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/PlannedPlanEntity.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/PlannedPlanEntity.kt index 5e03805..c538e0c 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/PlannedPlanEntity.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/PlannedPlanEntity.kt @@ -8,6 +8,7 @@ import jakarta.persistence.Id import jakarta.persistence.Table import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Persisted business-plan с устойчивым planId по unique business key. @@ -56,10 +57,10 @@ class PlannedPlanEntity( var dismissedAt: LocalDateTime? = null, @Column(name = "created_at", nullable = false) - var createdAt: LocalDateTime = LocalDateTime.now(), + var createdAt: LocalDateTime = UtcDateTimes.now(), @Column(name = "updated_at", nullable = false) - var updatedAt: LocalDateTime = LocalDateTime.now() + var updatedAt: LocalDateTime = UtcDateTimes.now() ) { /** Создаёт пустой экземпляр сущности, необходимый для JPA. */ constructor() : this( diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/ProcessedPlanDecisionEventEntity.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/ProcessedPlanDecisionEventEntity.kt index b594c13..9b66bbe 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/ProcessedPlanDecisionEventEntity.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/ProcessedPlanDecisionEventEntity.kt @@ -6,6 +6,7 @@ import jakarta.persistence.Id import jakarta.persistence.Table import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Dedup-запись Kafka decision event по eventId. @@ -18,7 +19,7 @@ class ProcessedPlanDecisionEventEntity( var eventId: UUID, @Column(name = "received_at", nullable = false) - var receivedAt: LocalDateTime = LocalDateTime.now() + var receivedAt: LocalDateTime = UtcDateTimes.now() ) { /** Создаёт пустой экземпляр сущности, необходимый для JPA. */ constructor() : this(eventId = UUID.randomUUID()) diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/SpacecraftPlanningStateEntity.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/SpacecraftPlanningStateEntity.kt index 9996614..4f0ba7b 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/SpacecraftPlanningStateEntity.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/SpacecraftPlanningStateEntity.kt @@ -7,6 +7,7 @@ import jakarta.persistence.Id import jakarta.persistence.Table import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes @Entity @Table(name = "spacecraft_planning_state") @@ -42,7 +43,7 @@ class SpacecraftPlanningStateEntity( var pausedAfterReject: Boolean = false, @Column(name = "updated_at", nullable = false) - var updatedAt: LocalDateTime = LocalDateTime.now() + var updatedAt: LocalDateTime = UtcDateTimes.now() ) { /** Создаёт пустой экземпляр сущности, необходимый для JPA. */ constructor() : this(spacecraftId = "") diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/SpacecraftSnapshotEntity.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/SpacecraftSnapshotEntity.kt index bd8cf2a..5d79281 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/SpacecraftSnapshotEntity.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/SpacecraftSnapshotEntity.kt @@ -5,6 +5,7 @@ import jakarta.persistence.Entity import jakarta.persistence.Id import jakarta.persistence.Table import java.time.LocalDateTime +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Persisted snapshot КА из НСИ, используемый как вход для refresh jobs. @@ -44,10 +45,10 @@ class SpacecraftSnapshotEntity( var active: Boolean = true, @Column(name = "created_at", nullable = false) - var createdAt: LocalDateTime = LocalDateTime.now(), + var createdAt: LocalDateTime = UtcDateTimes.now(), @Column(name = "updated_at", nullable = false) - var updatedAt: LocalDateTime = LocalDateTime.now() + var updatedAt: LocalDateTime = UtcDateTimes.now() ) { /** Создаёт пустой экземпляр сущности, необходимый для JPA. */ constructor() : this(spacecraftId = "", noradId = 0) diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/SpacecraftVisibilitySnapshotEntity.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/SpacecraftVisibilitySnapshotEntity.kt index 9aaa543..e129f11 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/SpacecraftVisibilitySnapshotEntity.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/SpacecraftVisibilitySnapshotEntity.kt @@ -5,6 +5,7 @@ import jakarta.persistence.Entity import jakarta.persistence.Id import jakarta.persistence.Table import java.time.LocalDateTime +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Полный raw snapshot окон видимости КА из баллистики. @@ -40,10 +41,10 @@ class SpacecraftVisibilitySnapshotEntity( var windowsCount: Int? = null, @Column(name = "created_at", nullable = false) - var createdAt: LocalDateTime = LocalDateTime.now(), + var createdAt: LocalDateTime = UtcDateTimes.now(), @Column(name = "updated_at", nullable = false) - var updatedAt: LocalDateTime = LocalDateTime.now() + var updatedAt: LocalDateTime = UtcDateTimes.now() ) { /** Создаёт пустой экземпляр сущности, необходимый для JPA. */ constructor() : this(spacecraftId = "", noradId = 0, payloadJson = "", payloadHash = "", fetchedAt = LocalDateTime.MIN) diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/VisibilityRefreshJobEntity.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/VisibilityRefreshJobEntity.kt index 272b2c5..9daee7b 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/VisibilityRefreshJobEntity.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/VisibilityRefreshJobEntity.kt @@ -7,6 +7,7 @@ import jakarta.persistence.Enumerated import jakarta.persistence.Id import jakarta.persistence.Table import java.time.LocalDateTime +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Job на перечитывание окон видимости одного КА у баллистики. @@ -41,10 +42,10 @@ class VisibilityRefreshJobEntity( var rerunRequested: Boolean = false, @Column(name = "requested_at", nullable = false) - var requestedAt: LocalDateTime = LocalDateTime.now(), + var requestedAt: LocalDateTime = UtcDateTimes.now(), @Column(name = "updated_at", nullable = false) - var updatedAt: LocalDateTime = LocalDateTime.now() + var updatedAt: LocalDateTime = UtcDateTimes.now() ) { /** Создаёт пустой экземпляр сущности, необходимый для JPA. */ constructor() : this(spacecraftId = "") diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/integration/api/ExternalPointsClient.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/integration/api/ExternalPointsClient.kt index 2e89f28..4f4e1a7 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/integration/api/ExternalPointsClient.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/integration/api/ExternalPointsClient.kt @@ -13,6 +13,7 @@ import org.springframework.web.reactive.function.client.WebClientException import org.springframework.web.reactive.function.client.WebClientResponseException import java.time.LocalDateTime import java.time.temporal.ChronoUnit +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Клиент внешнего API с окнами видимости КА. @@ -20,6 +21,7 @@ import java.time.temporal.ChronoUnit @Component class ExternalPointsClient( externalApiProperties: ExternalApiProperties, + webClientBuilder: WebClient.Builder, private val platformService: PlatformService, private val planningProperties: PlanningProperties, private val visibilityPayloadParser: VisibilityPayloadParser @@ -27,7 +29,7 @@ class ExternalPointsClient( private val log = LoggerFactory.getLogger(javaClass) - private val webClient: WebClient = WebClient.builder() + private val webClient: WebClient = webClientBuilder .baseUrl(externalApiProperties.baseUrl) .build() @@ -36,7 +38,7 @@ class ExternalPointsClient( * которые разрешены для работы TGU. */ fun fetchAllSpacecraftPoints(): List { - val timeStart = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES) + val timeStart = UtcDateTimes.now().truncatedTo(ChronoUnit.MINUTES) val timeStop = timeStart.plusDays(planningProperties.externalPointsLookaheadDays) return platformService.loadAllowedPlatforms() @@ -93,8 +95,8 @@ class ExternalPointsClient( .uri { uriBuilder -> uriBuilder .path("/api/satellites/{satelliteId}/rva") - .queryParam("time_start", timeStart) - .queryParam("time_stop", timeStop) + .queryParam("time_start", UtcDateTimes.format(timeStart)) + .queryParam("time_stop", UtcDateTimes.format(timeStop)) .build(satelliteId) } .retrieve() diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/integration/api/MissionPlaningClient.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/integration/api/MissionPlaningClient.kt index b5c4bed..fd4509f 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/integration/api/MissionPlaningClient.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/integration/api/MissionPlaningClient.kt @@ -4,6 +4,7 @@ import org.springframework.http.HttpStatus import org.springframework.stereotype.Component import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.server.ResponseStatusException +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import space.nstart.pcp_tgu_service.config.MissionPlaningProperties import java.time.Duration import java.time.LocalDateTime @@ -23,10 +24,25 @@ data class MissionBlankRequest( val missionStop: LocalDateTime ) -@Component -class MissionPlaningClient(properties: MissionPlaningProperties) { +private data class MissionCloneOutboundRequest( + val mode: String, + val windowStart: String? = null, + val windowStop: String? = null +) - private val webClient: WebClient = WebClient.builder() +private data class MissionBlankOutboundRequest( + val satelliteId: Long, + val missionStart: String, + val missionStop: String +) + +@Component +class MissionPlaningClient( + properties: MissionPlaningProperties, + webClientBuilder: WebClient.Builder +) { + + private val webClient: WebClient = webClientBuilder .baseUrl(properties.baseUrl) .build() @@ -35,7 +51,7 @@ class MissionPlaningClient(properties: MissionPlaningProperties) { fun cloneMission(missionId: UUID, req: MissionCloneRequest): MissionCloneResponse = webClient.post() .uri("/api/missions/{id}/clone", missionId) - .bodyValue(req) + .bodyValue(req.toOutbound()) .retrieve() .onStatus({ it.isError }) { resp -> resp.bodyToMono(String::class.java).defaultIfEmpty("").map { body -> @@ -53,11 +69,23 @@ class MissionPlaningClient(properties: MissionPlaningProperties) { "Пустой ответ mission-planing на клон миссии $missionId" ) + private fun MissionCloneRequest.toOutbound(): MissionCloneOutboundRequest = MissionCloneOutboundRequest( + mode = mode, + windowStart = windowStart?.let(UtcDateTimes::format), + windowStop = windowStop?.let(UtcDateTimes::format) + ) + + private fun MissionBlankRequest.toOutbound(): MissionBlankOutboundRequest = MissionBlankOutboundRequest( + satelliteId = satelliteId, + missionStart = UtcDateTimes.format(missionStart), + missionStop = UtcDateTimes.format(missionStop) + ) + /** Создаёт пустую миссию на окно (план новой цепочки «без истории»). */ fun createBlankMission(req: MissionBlankRequest): MissionCloneResponse = webClient.post() .uri("/api/missions/blank") - .bodyValue(req) + .bodyValue(req.toOutbound()) .retrieve() .onStatus({ it.isError }) { resp -> resp.bodyToMono(String::class.java).defaultIfEmpty("").map { body -> diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/integration/api/VisibilityPayloadParser.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/integration/api/VisibilityPayloadParser.kt index 1a1bca2..805862a 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/integration/api/VisibilityPayloadParser.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/integration/api/VisibilityPayloadParser.kt @@ -6,10 +6,8 @@ import space.nstart.pcp_tgu_service.domain.InsertionPoint import space.nstart.pcp_tgu_service.domain.ObservationWindow import space.nstart.pcp_tgu_service.domain.SpacecraftPoints import space.nstart.pcp_tgu_service.dto.ExternalRvaItemDto -import java.time.Instant +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime -import java.time.ZoneId -import java.time.format.DateTimeParseException import java.time.temporal.ChronoUnit /** @@ -43,21 +41,11 @@ class VisibilityPayloadParser( } /** - * Разбирает ISO-строку времени баллистики в naive `LocalDateTime`. + * Разбирает ISO-строку времени баллистики в UTC-naive `LocalDateTime`. * - * Если время приходит с зоной (`...Z`/офсет), момент проецируем в локальный пояс JVM - * (`ZoneId.systemDefault()`): naive-время в системе — настенные часы локальной зоны, в которой - * работают все сервисы, без UTC-семантики. Naive-строку без зоны берём как есть. - * - * Обрезаем до микросекунд — это эффективная точность системы: `planned_plan.start_time/end_time` - * хранятся как Postgres `timestamp(6)`, при записи наносекундный хвост баллистики всё равно теряется. - * Без обрезки проекция (наносекунды) и persisted-планы (микросекунды) перестают совпадать по - * бизнес-ключу — ломается дедуп в read-пути и матчинг базы в [calculatePlansAfter]. + * Явные UTC/offset-строки нормализуются в UTC, legacy naive-строки считаются UTC-naive. + * Обрезаем до микросекунд — это эффективная точность persisted-планов в БД. */ private fun toLocalDateTime(value: String): LocalDateTime = - try { - LocalDateTime.ofInstant(Instant.parse(value), ZoneId.systemDefault()) - } catch (_: DateTimeParseException) { - LocalDateTime.parse(value) - }.truncatedTo(ChronoUnit.MICROS) + UtcDateTimes.parse(value).truncatedTo(ChronoUnit.MICROS) } diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/AcceptedPlanSlotsUpdateJobService.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/AcceptedPlanSlotsUpdateJobService.kt index 63652f3..df53ba8 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/AcceptedPlanSlotsUpdateJobService.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/AcceptedPlanSlotsUpdateJobService.kt @@ -5,6 +5,7 @@ import org.springframework.transaction.annotation.Transactional import space.nstart.pcp_tgu_service.repository.AcceptedPlanSlotsUpdateJobRepository import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Создаёт persisted job обновления слотов для accepted plan. @@ -16,6 +17,6 @@ class AcceptedPlanSlotsUpdateJobService( /** Idempotent enqueue: один planId получает максимум одну job. */ @Transactional fun enqueue(planId: UUID) { - acceptedPlanSlotsUpdateJobRepository.insertPendingIfAbsent(planId, LocalDateTime.now()) + acceptedPlanSlotsUpdateJobRepository.insertPendingIfAbsent(planId, UtcDateTimes.now()) } } diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/AcceptedPlanSlotsUpdateWorker.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/AcceptedPlanSlotsUpdateWorker.kt index d5a8078..5d7705c 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/AcceptedPlanSlotsUpdateWorker.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/AcceptedPlanSlotsUpdateWorker.kt @@ -11,6 +11,7 @@ import space.nstart.pcp_tgu_service.repository.AcceptedPlanSlotsUpdateJobReposit import java.net.InetAddress import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Scale-out safe worker, который запускает updateAcceptedPlanSlots для accepted plan. @@ -35,7 +36,7 @@ class AcceptedPlanSlotsUpdateWorker( /** Claim видим для тестов; в production защищён FOR UPDATE SKIP LOCKED. */ fun claimJobs(limit: Int): List = transactionTemplate.execute { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val jobs = acceptedPlanSlotsUpdateJobRepository.findClaimableForUpdate(now, limit) jobs.forEach { job -> job.status = AcceptedPlanSlotsUpdateJobStatus.RUNNING @@ -69,7 +70,7 @@ class AcceptedPlanSlotsUpdateWorker( private fun markDone(planId: UUID, processInstanceKey: Long) { transactionTemplate.executeWithoutResult { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val job = acceptedPlanSlotsUpdateJobRepository.findById(planId).orElse(null) ?: return@executeWithoutResult job.status = AcceptedPlanSlotsUpdateJobStatus.DONE job.processInstanceKey = processInstanceKey @@ -83,7 +84,7 @@ class AcceptedPlanSlotsUpdateWorker( private fun markAmbiguous(planId: UUID, processInstanceKey: Long?, ex: Exception) { transactionTemplate.executeWithoutResult { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val job = acceptedPlanSlotsUpdateJobRepository.findById(planId).orElse(null) ?: return@executeWithoutResult job.status = AcceptedPlanSlotsUpdateJobStatus.AMBIGUOUS job.processInstanceKey = processInstanceKey ?: job.processInstanceKey diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/CreateChainPlanUseCase.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/CreateChainPlanUseCase.kt index 4740984..d03b58a 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/CreateChainPlanUseCase.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/CreateChainPlanUseCase.kt @@ -18,6 +18,7 @@ import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Создаёт план новой цепочки (вкладка «Планы», действие оператора «Создать план») и сразу считает @@ -46,7 +47,7 @@ class CreateChainPlanUseCase( private val log = LoggerFactory.getLogger(javaClass) fun createChainPlan(req: CreateChainPlanRequest): CreateChainPlanResponse { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() if (!req.windowStop.isAfter(req.windowStart)) { throw ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "windowStop должен быть позже windowStart") diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/CreateDraftUseCase.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/CreateDraftUseCase.kt index e679caa..e78f392 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/CreateDraftUseCase.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/CreateDraftUseCase.kt @@ -16,6 +16,7 @@ import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Создаёт черновик плана (DRAFT) на основе существующего базового плана. @@ -45,7 +46,7 @@ class CreateDraftUseCase( val base = plannedPlanRepository.findById(basePlanId) .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Plan $basePlanId not found") } - val now = LocalDateTime.now() + val now = UtcDateTimes.now() return when (req.mode) { DraftMode.REPLACE -> createReplaceDraft(base, now) diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/DismissPlanUseCase.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/DismissPlanUseCase.kt index 43666da..295abb0 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/DismissPlanUseCase.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/DismissPlanUseCase.kt @@ -9,6 +9,7 @@ import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * «Снять с учёта» / «Вернуть на учёт» терминально-негативный план. @@ -44,8 +45,8 @@ class DismissPlanUseCase( log.info("План {} уже снят с учёта ({}) — идемпотентно", planId, plan.dismissedAt) return } - plan.dismissedAt = LocalDateTime.now() - plan.updatedAt = LocalDateTime.now() + plan.dismissedAt = UtcDateTimes.now() + plan.updatedAt = UtcDateTimes.now() plannedPlanRepository.save(plan) log.info("План {} (status={}) снят с учёта", planId, plan.status) } @@ -59,7 +60,7 @@ class DismissPlanUseCase( return } plan.dismissedAt = null - plan.updatedAt = LocalDateTime.now() + plan.updatedAt = UtcDateTimes.now() plannedPlanRepository.save(plan) log.info("План {} (status={}) возвращён на учёт", planId, plan.status) } diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt index a7cf27f..b225742 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt @@ -20,6 +20,7 @@ import space.nstart.pcp_tgu_service.repository.ProcessedPlanDecisionEventReposit import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository import java.time.Duration import java.time.LocalDateTime +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Обрабатывает decision и закладочные feedback-события идемпотентно по eventId. @@ -132,7 +133,7 @@ class HandlePlanDecisionUseCase( ) } - val now = LocalDateTime.now() + val now = UtcDateTimes.now() plan.status = PlannedPlanStatus.LAID_IN plan.updatedAt = now @@ -213,7 +214,7 @@ class HandlePlanDecisionUseCase( return } - val now = LocalDateTime.now() + val now = UtcDateTimes.now() plan.status = terminalStatus plan.updatedAt = now plannedPlanRepository.save(plan) @@ -225,7 +226,7 @@ class HandlePlanDecisionUseCase( message: PlanDecisionMessage, attempt: PlanIssueAttemptEntity ) { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() attempt.status = when (message.decision) { PlanDecision.ACCEPTED -> PlanIssueAttemptStatus.ACCEPTED PlanDecision.REJECTED -> PlanIssueAttemptStatus.REJECTED @@ -241,7 +242,7 @@ class HandlePlanDecisionUseCase( attempt: PlanIssueAttemptEntity, plan: PlannedPlanEntity ) { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() recordDecisionOnAttempt(message, attempt) plan.status = PlannedPlanStatus.AWAITING_LAYIN @@ -262,7 +263,7 @@ class HandlePlanDecisionUseCase( plan: PlannedPlanEntity, state: SpacecraftPlanningStateEntity ) { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() recordDecisionOnAttempt(message, attempt) plan.status = PlannedPlanStatus.REJECTED @@ -307,7 +308,7 @@ class HandlePlanDecisionUseCase( } private fun claimEvent(eventId: java.util.UUID): Boolean = - processedPlanDecisionEventRepository.insertIfAbsent(eventId, LocalDateTime.now()) != 0 + processedPlanDecisionEventRepository.insertIfAbsent(eventId, UtcDateTimes.now()) != 0 private fun getOrCreateLockedState(spacecraftId: String): SpacecraftPlanningStateEntity { spacecraftPlanningStateJpaRepository.findById(spacecraftId).orElse(null) diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandleStationParamsChangedUseCase.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandleStationParamsChangedUseCase.kt index 7583db4..5cd135f 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandleStationParamsChangedUseCase.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandleStationParamsChangedUseCase.kt @@ -7,6 +7,7 @@ import space.nstart.pcp_tgu_service.dto.StationParamsChangedMessage import space.nstart.pcp_tgu_service.repository.ProcessedPlanDecisionEventRepository import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository import java.time.LocalDateTime +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * На изменение параметров станций перезапрашивает ЗРВ у баллистики по всем активным КА. @@ -25,7 +26,7 @@ class HandleStationParamsChangedUseCase( /** Идемпотентно по eventId; ставит refresh job по всем активным КА. */ @Transactional fun handle(message: StationParamsChangedMessage) { - val claimed = processedPlanDecisionEventRepository.insertIfAbsent(message.eventId, LocalDateTime.now()) != 0 + val claimed = processedPlanDecisionEventRepository.insertIfAbsent(message.eventId, UtcDateTimes.now()) != 0 if (!claimed) { return } diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/IssueDuePlanWorker.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/IssueDuePlanWorker.kt index 98dc84f..a550ede 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/IssueDuePlanWorker.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/IssueDuePlanWorker.kt @@ -20,6 +20,7 @@ import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaReposit import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Выдаёт дозревшие планы. В on-demand-модели голова цепочки считается из снапшота ЗРВ @@ -83,7 +84,7 @@ class IssueDuePlanWorker( /** Готовит выдачу головы проекции для КА под lock'ом состояния (scale-out safe). */ private fun prepareDueForSpacecraft(spacecraftId: String): PreparedAttempt? { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val state = getOrCreateLockedState(spacecraftId) // Форвард-цепочку не выдаём, пока по КА есть выдача «в работе»: ISSUING или WAITING_DECISION // в пределах grace после старта. Прошедший grace больше не блокирует — молчаливо продолжаем @@ -154,7 +155,7 @@ class IssueDuePlanWorker( .maxByOrNull { plan -> plan.endTime }?.planId ?: fallback private fun prepareSpecificPlan(planId: UUID): PreparedAttempt? { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val plan = plannedPlanRepository.findById(planId).orElse(null) ?: return null val state = getOrCreateLockedState(plan.spacecraftId) assertReissuableInSlot(plan, now) @@ -176,7 +177,7 @@ class IssueDuePlanWorker( } private fun prepareDraftPromotion(draftPlanId: UUID): PreparedAttempt? { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val plan = plannedPlanRepository.findById(draftPlanId).orElse(null) ?: return null if (plan.status != PlannedPlanStatus.DRAFT) { throw ResponseStatusException( @@ -326,7 +327,7 @@ class IssueDuePlanWorker( private fun markStarted(response: ProcessStartResponse) { transactionTemplate.executeWithoutResult { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val attempt = planIssueAttemptRepository.findById(response.attemptId).orElse(null) ?: return@executeWithoutResult val plan = plannedPlanRepository.findById(response.planId).orElse(null) ?: return@executeWithoutResult @@ -350,7 +351,7 @@ class IssueDuePlanWorker( private fun markAmbiguous(preparedAttempt: PreparedAttempt) { transactionTemplate.executeWithoutResult { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val attempt = planIssueAttemptRepository.findById(preparedAttempt.attemptId).orElse(null) ?: return@executeWithoutResult val plan = plannedPlanRepository.findById(preparedAttempt.planId).orElse(null) ?: return@executeWithoutResult attempt.status = PlanIssueAttemptStatus.AMBIGUOUS diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlanQueryService.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlanQueryService.kt index 0dc80cf..3d772d9 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlanQueryService.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlanQueryService.kt @@ -12,6 +12,7 @@ import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Read-сервис планов: объединяет persisted (выданные, история — вечно) с вычисленной @@ -80,7 +81,7 @@ class PlanQueryService( .sortedBy { response -> response.startTime } private fun historySince(historyDays: Long): LocalDateTime = - LocalDateTime.now().minusDays(historyDays.coerceAtLeast(0)) + UtcDateTimes.now().minusDays(historyDays.coerceAtLeast(0)) private fun PlannedPlanEntity.businessKey(): String = "$spacecraftId|$startTime|$endTime|$kppId" diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/SpacecraftVisibilitySnapshotService.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/SpacecraftVisibilitySnapshotService.kt index 7dde8c8..02b9b60 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/SpacecraftVisibilitySnapshotService.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/SpacecraftVisibilitySnapshotService.kt @@ -7,6 +7,7 @@ import space.nstart.pcp_tgu_service.integration.api.VisibilityPayloadParser import space.nstart.pcp_tgu_service.repository.SpacecraftVisibilitySnapshotRepository import java.security.MessageDigest import java.time.LocalDateTime +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Сохраняет raw visibility payload. Расписание считается on-demand из снапшота @@ -25,7 +26,7 @@ class SpacecraftVisibilitySnapshotService( val payloadHash = sha256(payloadJson) val existing = spacecraftVisibilitySnapshotRepository.findById(spacecraftId).orElse(null) val metadata = runCatching { extractMetadata(spacecraftId, payloadJson) }.getOrNull() - val now = LocalDateTime.now() + val now = UtcDateTimes.now() if (existing == null) { spacecraftVisibilitySnapshotRepository.save( diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/SyncSpacecraftFromNsiUseCase.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/SyncSpacecraftFromNsiUseCase.kt index 0c6fb08..ca3e890 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/SyncSpacecraftFromNsiUseCase.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/SyncSpacecraftFromNsiUseCase.kt @@ -5,7 +5,7 @@ import org.springframework.transaction.support.TransactionTemplate import space.nstart.pcp_tgu_service.domain.PlatformCatalogItem import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository import java.time.LocalDateTime -import java.time.ZoneId +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Синхронизирует разрешённые КА из НСИ в БД и ставит refresh jobs. @@ -26,7 +26,7 @@ class SyncSpacecraftFromNsiUseCase( } private fun persistAllowedSpacecraft(allowedPlatforms: List) { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val allowedSpacecraftIds = allowedPlatforms.mapNotNull { platform -> platform.noradId?.toString() } allowedPlatforms.forEach { platform -> @@ -40,10 +40,9 @@ class SyncSpacecraftFromNsiUseCase( name = platform.name, status = platform.status, mission = platform.mission, - // validFrom/validTo приходят как Instant; в naive-поля БД пишем настенные часы локальной - // зоны JVM (ZoneId.systemDefault()) — naive-время в системе локальное, без UTC-семантики. - validFrom = platform.validFrom?.let { LocalDateTime.ofInstant(it, ZoneId.systemDefault()) }, - validTo = platform.validTo?.let { LocalDateTime.ofInstant(it, ZoneId.systemDefault()) }, + // validFrom/validTo приходят как Instant; в naive-поля БД пишем UTC-naive момент. + validFrom = platform.validFrom?.let { LocalDateTime.ofInstant(it, UtcDateTimes.UTC) }, + validTo = platform.validTo?.let { LocalDateTime.ofInstant(it, UtcDateTimes.UTC) }, now = now ) visibilityRefreshJobService.enqueue(spacecraftId, noradId, "NSI_SYNC") diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/TestPlanDecisionService.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/TestPlanDecisionService.kt index 351f0fa..9ef2aeb 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/TestPlanDecisionService.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/TestPlanDecisionService.kt @@ -12,6 +12,7 @@ import space.nstart.pcp_tgu_service.repository.PlanIssueAttemptRepository import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository import java.time.LocalDateTime import java.util.UUID +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Test-only helper: генерирует decision event для последней (живой) attempt persisted plan. @@ -50,7 +51,7 @@ class TestPlanDecisionService( planId = plan.planId, attemptId = attempt.attemptId, decision = decision, - decisionTime = LocalDateTime.now(), + decisionTime = UtcDateTimes.now(), reason = reason?.takeIf { it.isNotBlank() } ?: DEFAULT_REASON ) handlePlanDecisionUseCase.handle(message) @@ -70,7 +71,7 @@ class TestPlanDecisionService( eventId = UUID.randomUUID(), spacecraftId = plan.spacecraftId, planId = plan.planId, - occurredAt = LocalDateTime.now(), + occurredAt = UtcDateTimes.now(), reason = reason?.takeIf { it.isNotBlank() } ?: DEFAULT_REASON ) handlePlanDecisionUseCase.handleLayinConfirmed(message) diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/VisibilityRefreshJobService.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/VisibilityRefreshJobService.kt index 79dd8fc..07da38d 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/VisibilityRefreshJobService.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/VisibilityRefreshJobService.kt @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import space.nstart.pcp_tgu_service.repository.VisibilityRefreshJobRepository import java.time.LocalDateTime +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Coalesce-сервис для visibility_refresh_job по spacecraftId. @@ -15,6 +16,6 @@ class VisibilityRefreshJobService( /** Ставит или переоткрывает refresh job без дублей. */ @Transactional fun enqueue(spacecraftId: String, noradId: Long?, reason: String) { - visibilityRefreshJobRepository.upsertPending(spacecraftId, noradId, reason, LocalDateTime.now()) + visibilityRefreshJobRepository.upsertPending(spacecraftId, noradId, reason, UtcDateTimes.now()) } } diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/VisibilityRefreshWorker.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/VisibilityRefreshWorker.kt index 8c62d17..2daeece 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/VisibilityRefreshWorker.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/VisibilityRefreshWorker.kt @@ -13,6 +13,7 @@ import space.nstart.pcp_tgu_service.repository.VisibilityRefreshJobRepository import java.net.InetAddress import java.time.LocalDateTime import java.time.temporal.ChronoUnit +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes /** * Claim'ит visibility_refresh_job, читает raw payload у баллистики и сохраняет snapshot. @@ -38,7 +39,7 @@ class VisibilityRefreshWorker( private fun claimJobs(limit: Int): List = transactionTemplate.execute { - val now = LocalDateTime.now() + val now = UtcDateTimes.now() val jobs = visibilityRefreshJobRepository.findPendingForUpdate(now, limit) jobs.forEach { job -> job.status = JobStatus.RUNNING @@ -61,10 +62,10 @@ class VisibilityRefreshWorker( } try { - val timeStart = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES) + val timeStart = UtcDateTimes.now().truncatedTo(ChronoUnit.MINUTES) val timeStop = timeStart.plusDays(planningProperties.externalPointsLookaheadDays) val payload = externalPointsClient.fetchRawVisibilityPayload(noradId, timeStart, timeStop) - spacecraftVisibilitySnapshotService.saveSnapshot(spacecraftId, noradId, payload, LocalDateTime.now()) + spacecraftVisibilitySnapshotService.saveSnapshot(spacecraftId, noradId, payload, UtcDateTimes.now()) markDone(spacecraftId) } catch (ex: Exception) { log.warn("Visibility refresh job failed. spacecraftId={}, noradId={}", spacecraftId, noradId, ex) @@ -79,7 +80,7 @@ class VisibilityRefreshWorker( job.rerunRequested = false job.lockedBy = null job.lockedUntil = null - job.updatedAt = LocalDateTime.now() + job.updatedAt = UtcDateTimes.now() visibilityRefreshJobRepository.save(job) } } @@ -90,7 +91,7 @@ class VisibilityRefreshWorker( job.status = if (job.attempts >= MAX_ATTEMPTS) JobStatus.FAILED else JobStatus.PENDING job.lockedBy = null job.lockedUntil = null - job.updatedAt = LocalDateTime.now() + job.updatedAt = UtcDateTimes.now() visibilityRefreshJobRepository.save(job) } } @@ -102,7 +103,7 @@ class VisibilityRefreshWorker( job.reason = reason job.lockedBy = null job.lockedUntil = null - job.updatedAt = LocalDateTime.now() + job.updatedAt = UtcDateTimes.now() visibilityRefreshJobRepository.save(job) } } diff --git a/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponseContractTest.kt b/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponseContractTest.kt index 18e0dd8..ecff626 100644 --- a/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponseContractTest.kt +++ b/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponseContractTest.kt @@ -1,25 +1,37 @@ package space.nstart.pcp_tgu_service.dto +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Test import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime import java.util.UUID /** - * Контракт формата времени на проводе для [PlanResponse]: naive `LocalDateTime` → ISO без зоны - * (напр. `"2026-05-29T01:00:00"`). Тест ловит случайный возврат к OffsetDateTime/Z или числовым меткам. + * Контракт формата времени на проводе для [PlanResponse]: UTC-naive `LocalDateTime` → ISO UTC с Z + * (напр. `"2026-05-29T01:00:00Z"`). Тест ловит случайный возврат к naive-строкам или числовым меткам. */ class PlanResponseContractTest { private val mapper = jacksonObjectMapper().findAndRegisterModules() + .registerModule( + SimpleModule().addSerializer(LocalDateTime::class.java, object : JsonSerializer() { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeString(UtcDateTimes.format(value)) + } + }) + ) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) @Test - fun `serializes plan times as naive ISO without zone`() { + fun `serializes plan times as UTC ISO with Z`() { val response = PlanResponse( planId = UUID.fromString("00000000-0000-0000-0000-000000000001"), spacecraftId = "25544", @@ -31,8 +43,8 @@ class PlanResponseContractTest { val node = mapper.readTree(mapper.writeValueAsString(response)) - assertEquals("2026-05-29T01:00:00", node.get("startTime").asText()) - assertEquals("2026-05-29T02:00:00", node.get("endTime").asText()) - assertFalse(node.get("startTime").asText().endsWith("Z"), "не ожидалась зона Z") + assertEquals("2026-05-29T01:00:00Z", node.get("startTime").asText()) + assertEquals("2026-05-29T02:00:00Z", node.get("endTime").asText()) + assertFalse(node.get("startTime").asText().contains("["), "не ожидался zoned date-time с ID зоны") } } diff --git a/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/integration/api/VisibilityPayloadParserTest.kt b/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/integration/api/VisibilityPayloadParserTest.kt index ffd17cf..c96163c 100644 --- a/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/integration/api/VisibilityPayloadParserTest.kt +++ b/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/integration/api/VisibilityPayloadParserTest.kt @@ -19,9 +19,8 @@ class VisibilityPayloadParserTest { } @Test - fun `projects zoned instant into JVM default zone as naive local LocalDateTime`() { - // naive-время в системе — настенные часы локальной зоны JVM (без UTC-семантики). Парсер проецирует - // зонированный инстант через ZoneId.systemDefault(), поэтому на МСК (+3) 14:14Z даёт локальные 17:14. + fun `normalizes zoned instant to UTC naive LocalDateTime`() { + // Зонированный инстант должен нормализоваться в UTC-naive время независимо от зоны JVM. TimeZone.setDefault(TimeZone.getTimeZone("Europe/Moscow")) val payload = """ [{ @@ -35,9 +34,8 @@ class VisibilityPayloadParserTest { val window = parser.parse("20", payload).points.single().observationWindows.single() - // Инстант 14:14Z спроецирован в локальную зону JVM (МСК, +3) — настенные 17:14. - assertEquals(LocalDateTime.parse("2026-06-02T17:14:08"), window.startTime) - assertEquals(LocalDateTime.parse("2026-06-02T17:20:00"), window.endTime) + assertEquals(LocalDateTime.parse("2026-06-02T14:14:08"), window.startTime) + assertEquals(LocalDateTime.parse("2026-06-02T14:20:00"), window.endTime) } @Test diff --git a/services/pcp-tgu-ui-service/src/api/ballisticsApi.ts b/services/pcp-tgu-ui-service/src/api/ballisticsApi.ts index e574081..6dd88b7 100644 --- a/services/pcp-tgu-ui-service/src/api/ballisticsApi.ts +++ b/services/pcp-tgu-ui-service/src/api/ballisticsApi.ts @@ -1,5 +1,5 @@ import { TguApiError } from '../model/tguTypes' -import { toLocalIso } from '../utils/localTime' +import { toUtcIso } from '../utils/localTime' import { apiFetch } from './apiFetch' const BASE = '/api/pcp-ballistics' @@ -67,7 +67,7 @@ export type PointVisibilityParam = { noradId: number /** Идентификатор объекта (цели) из запроса. */ objectId: string - /** Момент видимости (локальное время, ISO без зоны). */ + /** Момент видимости (UTC ISO). */ time: string /** Номер витка. */ revolution: number @@ -161,7 +161,7 @@ export async function fetchPointVisibility( /** Целеуказание (момент/угол места/азимут/дальность) границы зоны радиовидимости. */ export type RvaTargetPosition = { - /** Момент (локальное время, ISO без зоны). */ + /** Момент (UTC ISO). */ time: string /** Угол места, град. */ elevation: number @@ -233,13 +233,13 @@ export async function fetchSatelliteRva( } /** - * LocalDateTime без зоны (локальные настенные часы): YYYY-MM-DDTHH:mm:ss. + * UTC ISO date-time for API DTO/query params. * @param ms Время в миллисекундах от эпохи. * - * @returns Строка LocalDateTime без зоны. + * @returns Строка UTC date-time. */ function toLocalDateTime(ms: number): string { - return toLocalIso(ms) + return toUtcIso(ms) } function extractErrorText(body: string): string { diff --git a/services/pcp-tgu-ui-service/src/api/complexPlanApi.ts b/services/pcp-tgu-ui-service/src/api/complexPlanApi.ts index ccc655d..b05bdf8 100644 --- a/services/pcp-tgu-ui-service/src/api/complexPlanApi.ts +++ b/services/pcp-tgu-ui-service/src/api/complexPlanApi.ts @@ -1,5 +1,5 @@ import { TguApiError } from '../model/tguTypes' -import { toLocalIso } from '../utils/localTime' +import { toUtcIso } from '../utils/localTime' import { apiFetch } from './apiFetch' /** @@ -20,9 +20,9 @@ export const MAX_COMPLEX_PLAN_RANGE_MS = MAX_COMPLEX_PLAN_RANGE_DAYS * 24 * 60 * export type ComplexPlanRunStatus = 'CREATED' | 'RUNNING' | 'COMPLETED' | 'FAILED' export type RunComplexPlanParams = { - /** Начало интервала расчёта (эпоха-мс, локальное). */ + /** Начало интервала расчёта (эпоха-мс, абсолютный момент). */ fromMs: number - /** Конец интервала расчёта (эпоха-мс, локальное). */ + /** Конец интервала расчёта (эпоха-мс, абсолютный момент). */ toMs: number /** NORAD-id КА, по которым считаем (все эксплуатируемые КА вкладки «Планы»). */ satelliteIds: number[] @@ -82,7 +82,7 @@ export type ComplexPlanRunSummary = { coveredAreaPercent: number } -/** Режим съёмки/сброса из результата расчёта (SatelliteModeResponseDTO). Времена — локальные. */ +/** Режим съёмки/сброса из результата расчёта (SatelliteModeResponseDTO). Времена приходят из API как UTC ISO и отображаются в локальной зоне браузера. */ export type ComplexPlanMode = { id: number satelliteId: number @@ -115,8 +115,8 @@ export type ComplexPlanMode = { */ export async function runComplexPlan(params: RunComplexPlanParams): Promise { const body = { - intervalStart: toLocalIso(params.fromMs), - intervalEnd: toLocalIso(params.toMs), + intervalStart: toUtcIso(params.fromMs), + intervalEnd: toUtcIso(params.toMs), satelliteIds: params.satelliteIds, } diff --git a/services/pcp-tgu-ui-service/src/api/missionPlanningApi.ts b/services/pcp-tgu-ui-service/src/api/missionPlanningApi.ts index 7f44af0..2709233 100644 --- a/services/pcp-tgu-ui-service/src/api/missionPlanningApi.ts +++ b/services/pcp-tgu-ui-service/src/api/missionPlanningApi.ts @@ -9,7 +9,7 @@ export type SurveyRouteAnchor = 'CENTERED' | 'FORWARD' export type BuildSurveyRouteParams = { /** Идентификатор КА (NORAD — тот же, что в obj-view/mpl-point). */ satelliteId: number - /** Момент видимости (локальное время, ISO без зоны). */ + /** Момент видимости (UTC ISO). */ time: string /** Длительность съёмки, секунды. */ durationSeconds: number @@ -22,17 +22,17 @@ export type BuildSurveyRouteParams = { /** Способ построения окна относительно момента видимости. */ anchor?: SurveyRouteAnchor /** - * Начало интервала плана (локальное время, ISO без зоны). Передаётся при сохранении, + * Начало интервала плана (UTC ISO). Передаётся при сохранении, * чтобы при автосоздании миссии/плана задать корректное окно (для пересечений/наложений). */ planStart?: string - /** Конец интервала плана (локальное время, ISO без зоны). См. planStart. */ + /** Конец интервала плана (UTC ISO). См. planStart. */ planEnd?: string } export type SurveyRoute = { satelliteId: number - /** Начало съёмки (локальное время, ISO без зоны). */ + /** Начало съёмки (UTC ISO). */ timeStart: string /** Длительность съёмки, секунды. */ duration: number @@ -87,9 +87,9 @@ export type SaveDropRouteParams = { station: string /** Номер витка зоны радиовидимости. */ revolution: number - /** Начало зоны (локальное время, ISO без зоны). */ + /** Начало зоны (UTC ISO). */ zoneStart: string - /** Конец зоны (локальное время, ISO без зоны). */ + /** Конец зоны (UTC ISO). */ zoneStop: string /** Идентификаторы режимов съёмки (survey_modes), сбрасываемых в этой зоне. */ surveyModeIds: number[] @@ -168,7 +168,7 @@ export async function saveDropRoute( return JSON.parse(text) as DropMode } -/** Итог расчёта плана съёмки (ответ /surveys/calculate). Времена — локальные. */ +/** Итог расчёта плана съёмки (ответ /surveys/calculate). Времена приходят из API как UTC ISO и отображаются UI в локальной зоне браузера. */ export type SurveyCalculationResult = { missionId: string completed: boolean diff --git a/services/pcp-tgu-ui-service/src/api/tguPlanApi.ts b/services/pcp-tgu-ui-service/src/api/tguPlanApi.ts index f6a97ac..55d5e23 100644 --- a/services/pcp-tgu-ui-service/src/api/tguPlanApi.ts +++ b/services/pcp-tgu-ui-service/src/api/tguPlanApi.ts @@ -12,7 +12,7 @@ export type DraftMode = 'replace' | 'fork-forward' export type CreateDraftRequest = { mode: DraftMode - /** Начало окна целевой ЗРВ (naive, ISO без зоны, локальное). Обязательно для fork-forward. */ + /** Начало окна целевой ЗРВ (UTC ISO). Обязательно для fork-forward. */ windowStart?: string /** Конец окна целевой ЗРВ. Обязательно для fork-forward. */ windowStop?: string @@ -54,7 +54,7 @@ export type ZrvWindowResponse = { /** * Запрос создания плана новой цепочки (вкладка «Планы»). Окно набирается из двух ЗРВ: * `windowStart`/`windowKppId` — старт выбранной стартовой зоны, `windowStop` — конец конечной. - * Времена — naive (ISO без зоны, локальное), как ждёт `LocalDateTime` бэка. `basePlanId` — предыстория + * Времена отправляются в API как UTC ISO с `Z`. `basePlanId` — предыстория * (история тянется) либо `null` (цепочка без истории, пустой план). */ export type CreateChainPlanRequest = { diff --git a/services/pcp-tgu-ui-service/src/features/tgu-complex-plan/TguComplexPlanTab.tsx b/services/pcp-tgu-ui-service/src/features/tgu-complex-plan/TguComplexPlanTab.tsx index d56145f..ae63e4f 100644 --- a/services/pcp-tgu-ui-service/src/features/tgu-complex-plan/TguComplexPlanTab.tsx +++ b/services/pcp-tgu-ui-service/src/features/tgu-complex-plan/TguComplexPlanTab.tsx @@ -20,7 +20,7 @@ import type { MapPolygon, Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes"; import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes"; import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView"; import { MapObjectInspector } from "../tgu-map-2d/MapObjectInspector"; -import { formatLocalDateTime, parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime"; +import { formatLocalDateTime, parseLocal, toLocalInputValue, toUtcIso } from "../../utils/localTime"; import { formatVolume } from "../../utils/dataVolume"; type TguComplexPlanTabProps = { @@ -173,7 +173,7 @@ export function TguComplexPlanTab({ range, platforms }: TguComplexPlanTabProps) let cancelled = false; setRequestsLoading(true); setRequestsError(undefined); - fetchRequestsForMap({ endFrom: toLocalIso(fromMs), beginTo: toLocalIso(toMs) }) + fetchRequestsForMap({ endFrom: toUtcIso(fromMs), beginTo: toUtcIso(toMs) }) .then((result) => { if (!cancelled) setRequestItems(result.items); }) diff --git a/services/pcp-tgu-ui-service/src/features/tgu-editor/TguEditorTab.tsx b/services/pcp-tgu-ui-service/src/features/tgu-editor/TguEditorTab.tsx index 30081f5..c7d96da 100644 --- a/services/pcp-tgu-ui-service/src/features/tgu-editor/TguEditorTab.tsx +++ b/services/pcp-tgu-ui-service/src/features/tgu-editor/TguEditorTab.tsx @@ -18,7 +18,7 @@ import { import { abandonDraft, createDraft, fetchDraftPlans } from "../../api/tguPlanApi"; import { TguApiError } from "../../model/tguTypes"; import type { TguPlanUi } from "../../model/timelineTypes"; -import { parseLocal, toLocalIso } from "../../utils/localTime"; +import { parseLocal, toUtcIso } from "../../utils/localTime"; import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView"; import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser"; import { buildStationMarkers } from "../tgu-map-2d/Tgu2DMapLayers"; @@ -360,8 +360,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, useEffect(() => { const controller = new AbortController(); void fetchRequestsForMap({ - endFrom: toLocalIso(window.fromMs), - beginTo: toLocalIso(window.toMs) + endFrom: toUtcIso(window.fromMs), + beginTo: toUtcIso(window.toMs) }).then((result) => { if (!controller.signal.aborted) { setRequestItems(result.items); @@ -458,7 +458,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, void buildSurveyRoute({ satelliteId: numericNorad, // Окно задано явно (начало/конец) — строим вперёд от начала на выбранную длительность. - time: toLocalIso(routeWindow.startMs), + time: toUtcIso(routeWindow.startMs), durationSeconds, roll: routeRoll, captureAngle: DEFAULT_CAPTURE_ANGLE_DEG, @@ -617,9 +617,9 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, noradId: noradId ? Number(noradId) : 0, stationId: Number.isFinite(stationNum) ? stationNum : 0, revolution: work.revolution ?? 0, - onStart: { time: toLocalIso(work.startMs).slice(0, 19), elevation: 0, azimuth: 0, range: 0 }, - onMaximum: { time: toLocalIso(midMs).slice(0, 19), elevation: 0, azimuth: 0, range: 0 }, - onStop: { time: toLocalIso(work.endMs).slice(0, 19), elevation: 0, azimuth: 0, range: 0 }, + onStart: { time: toUtcIso(work.startMs), elevation: 0, azimuth: 0, range: 0 }, + onMaximum: { time: toUtcIso(midMs), elevation: 0, azimuth: 0, range: 0 }, + onStop: { time: toUtcIso(work.endMs), elevation: 0, azimuth: 0, range: 0 }, duration: Math.max(1, (work.endMs - work.startMs) / 1000) }; editDropPrefillRef.current = { zone, surveyIds: work.surveyIds ?? [] }; @@ -744,15 +744,15 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, if (editWork) await deleteEditedModeFromDraft(draftId, draftExistedBefore, editWork, "SURVEY"); savedRoute = await saveSurveyRoute(draftId, { satelliteId: numericNorad, - time: toLocalIso(routeWindow.startMs), + time: toUtcIso(routeWindow.startMs), durationSeconds: routeDurationSeconds, roll: routeRoll, captureAngle: DEFAULT_CAPTURE_ANGLE_DEG, revolution: selectedShootPass.revolution ?? 0, anchor: "FORWARD", // Окно выбранного плана — чтобы при автосоздании миссии/плана задать корректный интервал. - planStart: toLocalIso(selectedPlan.startMs), - planEnd: toLocalIso(selectedPlan.endMs) + planStart: toUtcIso(selectedPlan.startMs), + planEnd: toUtcIso(selectedPlan.endMs) }); setSurveyRoute(savedRoute); // Ждём перечитывания planModes ДО снятия editingWorkId ниже: иначе на миг editingModeId @@ -853,8 +853,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, zoneStop: selectedZone.onStop.time.slice(0, 19), surveyModeIds: [...selectedDropSurveyIds], // Окно выбранного плана — для автосоздания миссии/плана с корректным интервалом. - planStart: toLocalIso(selectedPlan.startMs), - planEnd: toLocalIso(selectedPlan.endMs) + planStart: toUtcIso(selectedPlan.startMs), + planEnd: toUtcIso(selectedPlan.endMs) }); savedDropId = savedDrop.id; savedDropVolumeMb = savedDrop.volumeMb; diff --git a/services/pcp-tgu-ui-service/src/features/tgu-map-2d/Tgu2DMapTab.tsx b/services/pcp-tgu-ui-service/src/features/tgu-map-2d/Tgu2DMapTab.tsx index 7d8ec08..3f08c65 100644 --- a/services/pcp-tgu-ui-service/src/features/tgu-map-2d/Tgu2DMapTab.tsx +++ b/services/pcp-tgu-ui-service/src/features/tgu-map-2d/Tgu2DMapTab.tsx @@ -13,7 +13,7 @@ import { Tgu2DMapSidebar } from "./Tgu2DMapSidebar"; import { Tgu2DMapToolbar } from "./Tgu2DMapToolbar"; import { Tgu2DMapView } from "./Tgu2DMapView"; import { getTgu2DMapIntervalState, TGU_2D_MAP_INTERVAL_WARNING } from "./tgu2DMapInterval"; -import { parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime"; +import { parseLocal, toLocalInputValue, toUtcIso } from "../../utils/localTime"; type Tgu2DMapTabProps = { range: TimelineRange; @@ -82,8 +82,8 @@ export function Tgu2DMapTab({ try { const result = await fetchRequestsForMap( { - endFrom: toLocalIso(fromMs), - beginTo: toLocalIso(toMs) + endFrom: toUtcIso(fromMs), + beginTo: toUtcIso(toMs) }, (loaded, total) => { setRequestsState((current) => ({ ...current, loadedCount: loaded, totalItems: total })); diff --git a/services/pcp-tgu-ui-service/src/features/tgu-planning/createChainWindows.test.ts b/services/pcp-tgu-ui-service/src/features/tgu-planning/createChainWindows.test.ts index f35c17d..9434b95 100644 --- a/services/pcp-tgu-ui-service/src/features/tgu-planning/createChainWindows.test.ts +++ b/services/pcp-tgu-ui-service/src/features/tgu-planning/createChainWindows.test.ts @@ -22,10 +22,10 @@ describe("futureZrvOptions", () => { ]); }); - it("produces naive (no zone) start/stop strings", () => { + it("produces UTC start/stop strings", () => { const [option] = futureZrvOptions([zrv("2026-06-03T13:00:00", "2026-06-03T13:10:00")], NOW); - expect(option.start).toBe("2026-06-03T13:00:00"); - expect(option.stop).toBe("2026-06-03T13:10:00"); + expect(option.start).toBe(new Date(2026, 5, 3, 13, 0, 0).toISOString()); + expect(option.stop).toBe(new Date(2026, 5, 3, 13, 10, 0).toISOString()); expect(option.kppId).toBe("KPP-1"); }); }); @@ -33,11 +33,11 @@ describe("futureZrvOptions", () => { describe("buildChainPlanRequest", () => { const start: ZrvOption = { id: "a", startMs: Date.UTC(2026, 5, 3, 13, 0, 0), endMs: Date.UTC(2026, 5, 3, 13, 10, 0), - start: "2026-06-03T13:00:00", stop: "2026-06-03T13:10:00", kppId: "KPP-1" + start: "2026-06-03T13:00:00.000Z", stop: "2026-06-03T13:10:00.000Z", kppId: "KPP-1" }; const end: ZrvOption = { id: "b", startMs: Date.UTC(2026, 5, 3, 15, 0, 0), endMs: Date.UTC(2026, 5, 3, 15, 10, 0), - start: "2026-06-03T15:00:00", stop: "2026-06-03T15:10:00", kppId: "KPP-2" + start: "2026-06-03T15:00:00.000Z", stop: "2026-06-03T15:10:00.000Z", kppId: "KPP-2" }; it("spans from start zone begin to end zone finish and keeps the start kpp", () => { @@ -45,16 +45,16 @@ describe("buildChainPlanRequest", () => { expect(req).toEqual({ spacecraftId: "25544", basePlanId: "base-1", - windowStart: "2026-06-03T13:00:00", - windowStop: "2026-06-03T15:10:00", + windowStart: "2026-06-03T13:00:00.000Z", + windowStop: "2026-06-03T15:10:00.000Z", windowKppId: "KPP-1" }); }); it("allows a single-zone window (start == end zone)", () => { const req = buildChainPlanRequest("25544", null, start, start); - expect(req.windowStart).toBe("2026-06-03T13:00:00"); - expect(req.windowStop).toBe("2026-06-03T13:10:00"); + expect(req.windowStart).toBe("2026-06-03T13:00:00.000Z"); + expect(req.windowStop).toBe("2026-06-03T13:10:00.000Z"); expect(req.basePlanId).toBeNull(); }); diff --git a/services/pcp-tgu-ui-service/src/features/tgu-planning/createChainWindows.ts b/services/pcp-tgu-ui-service/src/features/tgu-planning/createChainWindows.ts index 88bc613..28fd95a 100644 --- a/services/pcp-tgu-ui-service/src/features/tgu-planning/createChainWindows.ts +++ b/services/pcp-tgu-ui-service/src/features/tgu-planning/createChainWindows.ts @@ -1,9 +1,9 @@ -import { parseLocal, toLocalIso } from "../../utils/localTime"; +import { parseLocal, toUtcIso } from "../../utils/localTime"; import type { CreateChainPlanRequest, ZrvWindowResponse } from "../../api/tguPlanApi"; /** * Опция выбора ЗРВ в диалоге «Создать план». Из стартовой зоны берутся начало окна и КПП, из - * конечной — конец окна. `start`/`stop` — naive (ISO без зоны, локальное), как ждёт `LocalDateTime` бэка; + * конечной — конец окна. `start`/`stop` — UTC ISO строки для API; * `startMs`/`endMs` — для сортировки/сравнения и подсветки на таймлайне. */ export type ZrvOption = { @@ -11,9 +11,9 @@ export type ZrvOption = { id: string; startMs: number; endMs: number; - /** Naive (локальное) начала зоны. */ + /** UTC ISO начала зоны. */ start: string; - /** Naive (локальное) конца зоны. */ + /** UTC ISO конца зоны. */ stop: string; kppId: string; }; @@ -28,8 +28,8 @@ export function futureZrvOptions(windows: ZrvWindowResponse[], nowMs: number): Z id: `${zrv.kppId}|${zrv.startTime}|${zrv.endTime}`, startMs, endMs, - start: toNaive(startMs), - stop: toNaive(endMs), + start: toApiUtc(startMs), + stop: toApiUtc(endMs), kppId: zrv.kppId }; }) @@ -60,6 +60,6 @@ export function buildChainPlanRequest( }; } -function toNaive(ms: number): string { - return toLocalIso(ms); +function toApiUtc(ms: number): string { + return toUtcIso(ms); } diff --git a/services/pcp-tgu-ui-service/src/features/tgu-requests/TguRequestsTab.tsx b/services/pcp-tgu-ui-service/src/features/tgu-requests/TguRequestsTab.tsx index 8c96420..c54b16e 100644 --- a/services/pcp-tgu-ui-service/src/features/tgu-requests/TguRequestsTab.tsx +++ b/services/pcp-tgu-ui-service/src/features/tgu-requests/TguRequestsTab.tsx @@ -9,7 +9,7 @@ import type { RequestAtPoint } from "./RequestDetailsPanel"; import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes"; import type { GeoPoint, MapPolygon } from "../tgu-map-2d/model/mapTypes"; import { generateUuid } from "../../utils/uuid"; -import { parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime"; +import { parseLocal, toLocalInputValue, toUtcIso } from "../../utils/localTime"; import { DateTime24Field } from "../../components/timeFields"; import { RequestCreatePanel, type RequestFormSubmit } from "./RequestCreatePanel"; import { RequestDetailsPanel } from "./RequestDetailsPanel"; @@ -82,7 +82,7 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa setRequestsState((current) => ({ ...current, loading: true, error: undefined, loadedCount: 0 })); try { const result = await fetchRequestsForMap( - { endFrom: toLocalIso(fromMs), beginTo: toLocalIso(toMs) }, + { endFrom: toUtcIso(fromMs), beginTo: toUtcIso(toMs) }, (loaded, total) => setRequestsState((current) => ({ ...current, loadedCount: loaded, totalItems: total })) ); setRequestsState((current) => ({ @@ -204,8 +204,8 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa name: form.name, geometry, importance: form.importance, - beginDateTime: toLocalIso(parseLocal(form.beginDateTime)), - endDateTime: toLocalIso(parseLocal(form.endDateTime)), + beginDateTime: toUtcIso(parseLocal(form.beginDateTime)), + endDateTime: toUtcIso(parseLocal(form.endDateTime)), kpp: form.kpp, highPriorityTransmit: form.highPriorityTransmit, optics: form.optics, diff --git a/services/pcp-tgu-ui-service/src/features/tgu-slots/TguSlotsTab.tsx b/services/pcp-tgu-ui-service/src/features/tgu-slots/TguSlotsTab.tsx index b632634..72be661 100644 --- a/services/pcp-tgu-ui-service/src/features/tgu-slots/TguSlotsTab.tsx +++ b/services/pcp-tgu-ui-service/src/features/tgu-slots/TguSlotsTab.tsx @@ -23,7 +23,7 @@ import type { Tgu2DMapLayersState } from "../tgu-map-2d/model/mapLayerTypes"; import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes"; import { MapObjectInspector } from "../tgu-map-2d/MapObjectInspector"; import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView"; -import { formatLocalDateTimeSeconds, parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime"; +import { formatLocalDateTimeSeconds, parseLocal, toLocalInputValue, toUtcIso } from "../../utils/localTime"; import { SlotDaySelector, type SlotDay } from "./SlotDaySelector"; import { PassStepper } from "./PassStepper"; import { groupPasses } from "./slotPasses"; @@ -469,7 +469,7 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) { setRequestsLoading(true); setRequestsError(undefined); try { - const result = await fetchRequestsForMap({ endFrom: toLocalIso(fromMs), beginTo: toLocalIso(toMs) }); + const result = await fetchRequestsForMap({ endFrom: toUtcIso(fromMs), beginTo: toUtcIso(toMs) }); setRequestItems(result.items); setRequestsTotal(result.totalItems); setRequestsTruncated(result.truncated); @@ -730,8 +730,8 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) { const result = await fetchRequestSlots( { requestId: selectedRequestId, - timeStart: toLocalIso(parsedRange.fromMs), - timeStop: toLocalIso(parsedRange.toMs), + timeStart: toUtcIso(parsedRange.fromMs), + timeStop: toUtcIso(parsedRange.toMs), revSign: revSign || undefined, // cov=true (режим покрытия) ровно когда задана стратегия — это путь «Подобрать // покрытие». «Показать все слоты» идёт без стратегии и получает полный набор кандидатов, diff --git a/services/pcp-tgu-ui-service/src/utils/localTime.ts b/services/pcp-tgu-ui-service/src/utils/localTime.ts index fe249e4..b424547 100644 --- a/services/pcp-tgu-ui-service/src/utils/localTime.ts +++ b/services/pcp-tgu-ui-service/src/utils/localTime.ts @@ -1,14 +1,16 @@ /** - * Единый разбор и форматирование времени в локальной зоне. + * Единые утилиты времени для UI. * - * Бэкенд (Kotlin `LocalDateTime`) отдаёт время как ISO-строку без зоны - * (например `"2026-05-29T01:00:00"`) — это настенные часы локальной зоны, в которой - * работают все сервисы, без UTC-семантики. Браузер по умолчанию трактует такую naive-строку - * как локальное время — это нам и нужно: разбор, показ и ввод идут в зоне браузера, а - * сериализация назад отдаёт те же настенные часы naive-строкой без зоны. + * Контракт API после UTC-перевода: + * - backend/БД/DTO используют UTC; + * - API отдаёт ISO-8601 UTC с `Z`; + * - UI показывает время в часовом поясе браузера; + * - значения из локальных полей ввода перед отправкой в API конвертируются в UTC ISO. * - * Строки с явной зоной (`...Z`/офсет, напр. валидность платформ из НСИ) парсятся как - * абсолютный момент и показываются в локальной зоне. + * Legacy-строки API без зоны всё ещё могут встретиться при неполностью обновлённых сервисах. + * В основном UI они обычно приходят из локальных input-полей, поэтому `parseLocal` сохраняет старое + * поведение: naive-строка разбирается как локальное время браузера. После полного перевода API + * новые ответы должны приходить с `Z` и будут разобраны как абсолютный UTC-момент. */ function pad(n: number): string { @@ -60,10 +62,15 @@ export function toLocalInputValue(ms: number): string { } /** - * naive `LocalDateTime` без зоны (`YYYY-MM-DDTHH:mm:ss`) из локальных компонент эпохи-мс. - * Все бэкенды ожидают `LocalDateTime` без зоны — настенные часы локальной зоны. + * Локальная ISO-строка без зоны (`YYYY-MM-DDTHH:mm:ss`) из локальных компонент. + * Оставлена для локальных UI-расчётов и полей времени, не использовать для отправки API DTO. */ export function toLocalIso(ms: number): string { const d = new Date(ms); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } + +/** UTC ISO-строка для DTO/query-параметров API. */ +export function toUtcIso(ms: number): string { + return new Date(ms).toISOString(); +} diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/configuration/UtcTimeConfiguration.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/configuration/UtcTimeConfiguration.kt new file mode 100644 index 0000000..197eae7 --- /dev/null +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/configuration/UtcTimeConfiguration.kt @@ -0,0 +1,55 @@ +package space.nstart.pcp.slots_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 pcpUiUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder -> + builder.addModule( + SimpleModule("pcp-ui-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 { + override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source) +} + +private class UtcLocalDateTimeSerializer : StdSerializer(LocalDateTime::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class UtcLocalDateTimeDeserializer : StdDeserializer(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) + } +} diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/BookingsController.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/BookingsController.kt index f9cf81b..92cd3cd 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/BookingsController.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/BookingsController.kt @@ -1,6 +1,5 @@ package space.nstart.pcp.slots_service.controller -import org.springframework.format.annotation.DateTimeFormat import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam @@ -46,8 +45,8 @@ class BookingsController( @GetMapping fun bookedSlots( - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) timeStart: LocalDateTime?, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) timeStop: LocalDateTime?, + @RequestParam(required = false) timeStart: LocalDateTime?, + @RequestParam(required = false) timeStop: LocalDateTime?, @RequestParam(required = false) requestId: String?, @RequestParam(required = false) satelliteIds: List?, ): List { diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/CatalogRestController.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/CatalogRestController.kt index fba882f..70f9369 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/CatalogRestController.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/CatalogRestController.kt @@ -27,6 +27,7 @@ import space.nstart.pcp.slots_service.service.BallisticsService import space.nstart.pcp.slots_service.service.SatelliteCatalogService import space.nstart.pcp.slots_service.service.SatellitePdcmService import space.nstart.pcp.slots_service.service.SlotService +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException @@ -103,14 +104,16 @@ class CatalogRestController( private fun parseSendInitialConditionsTime(time: String?): LocalDateTime { val normalizedTime = time?.trim()?.takeIf { it.isNotEmpty() } - ?: return LocalDateTime.now() + ?: return UtcDateTimes.now() + + UtcDateTimes.parseOrNull(normalizedTime)?.let { return it } return try { LocalDateTime.parse(normalizedTime, sendInitialConditionsTimeFormatter) } catch (exception: DateTimeParseException) { throw ResponseStatusException( HttpStatus.BAD_REQUEST, - "Параметр time должен быть в формате dd.mm.yyyy hh:MM:ss.zzz", + "Параметр time должен быть ISO-8601 UTC/offset или legacy dd.mm.yyyy hh:MM:ss.zzz", exception ) } diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/DynamicPlanController.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/DynamicPlanController.kt index c32b6fc..b67edb7 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/DynamicPlanController.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/DynamicPlanController.kt @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RestController import space.nstart.pcp.pcp_types_lib.dto.ballistics.AngularMotionCalculationRequestDTO import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationRequestDTO import space.nstart.pcp.slots_service.service.DynamicPlanService +import java.time.LocalDateTime import java.util.UUID @Controller @@ -83,14 +84,14 @@ class AngularMotionRestController( ) { @GetMapping("/satellites") @ResponseBody - fun satellites(@RequestParam(required = false) time: String?) = + fun satellites(@RequestParam(required = false) time: LocalDateTime?) = dynamicPlanService.getAngularMotionSatellites(time) @GetMapping("/satellites/{satelliteId}/flight-line") @ResponseBody fun flightLine( @PathVariable satelliteId: Long, - @RequestParam time: String + @RequestParam time: LocalDateTime ) = dynamicPlanService.getAngularMotionFlightLine(satelliteId, time) @PostMapping("/calculate") diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/MapController.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/MapController.kt index fd58c0f..8b91420 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/MapController.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/MapController.kt @@ -41,7 +41,7 @@ class MapController { fun showMap(model: Model): Mono { model.addAttribute("requests", earthService.reqs().collectList().block()) model.addAttribute("satellites", complexMissionService.satellites().collectList().block()) - model.addAttribute("stations", stationService.stations().collectList().block()) + model.addAttribute("stations", stationService.allStationsOrEmpty()) return Mono.just("map") } diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/TleComparisonController.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/TleComparisonController.kt index 0cf22ed..af5c07f 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/TleComparisonController.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/TleComparisonController.kt @@ -15,9 +15,9 @@ import space.nstart.pcp.slots_service.dto.tlecomparison.TlePositionDTO import space.nstart.pcp.slots_service.dto.tlecomparison.TlePositionDeltaDTO import space.nstart.pcp.slots_service.dto.tlecomparison.TleTextRequestDTO import space.nstart.pcp.slots_service.service.BallisticsService +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.Duration import java.time.LocalDateTime -import java.time.format.DateTimeFormatter import java.util.Locale import kotlin.math.abs import kotlin.math.sqrt @@ -144,14 +144,13 @@ class TleComparisonController( ) private fun timeRow(code: String, name: String, first: LocalDateTime?, second: LocalDateTime?): TleCompareRowDTO { - val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME val delta = if (first != null && second != null) { val seconds = Duration.between(first, second).seconds formatSecondsDelta(seconds) } else { null } - return TleCompareRowDTO(code, name, first?.format(formatter), second?.format(formatter), delta) + return TleCompareRowDTO(code, name, first?.let(UtcDateTimes::format), second?.let(UtcDateTimes::format), delta) } private fun numberRow(code: String, name: String, first: Double?, second: Double?): TleCompareRowDTO = diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/czml/CZMLClock.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/czml/CZMLClock.kt index 6416d0a..4b5a096 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/czml/CZMLClock.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/czml/CZMLClock.kt @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema import lombok.AllArgsConstructor import lombok.Data import lombok.NoArgsConstructor +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @@ -23,13 +24,8 @@ class CZMLClock( var step : String = "SYSTEM_CLOCK_MULTIPLIER" ) { constructor(tn : LocalDateTime, tk : LocalDateTime, cur : LocalDateTime) : this( - interval ="${ - tn.toString() - }/${ - tk - .toString() - }", - currentTime = cur.toString() + interval = "${UtcDateTimes.format(tn)}/${UtcDateTimes.format(tk)}", + currentTime = UtcDateTimes.format(cur) ) } \ No newline at end of file diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/czml/CZMLPath.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/czml/CZMLPath.kt index 92c24fe..ea23a55 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/czml/CZMLPath.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/czml/CZMLPath.kt @@ -7,6 +7,7 @@ import lombok.AllArgsConstructor import lombok.Data import lombok.NoArgsConstructor import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime import java.time.ZoneOffset @@ -28,6 +29,10 @@ class CZMLPath( private fun fromDateTime(t: LocalDateTime) = t.toEpochSecond(ZoneOffset.UTC) + t.nano / 1e9 + private fun utcIso(t: Double): String = UtcDateTimes.format( + LocalDateTime.ofEpochSecond(t.toLong(), ((t % 1) * 1e9).toInt(), ZoneOffset.UTC) + ) + constructor( vuz: Iterable, epoch: LocalDateTime, epochStop: LocalDateTime, width: Int, resolution: Int, material: CZMLMaterial @@ -44,26 +49,16 @@ class CZMLPath( if (dt > 1) { lt.add( CZMLInterval( - "${ - LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC) - }/${ - LocalDateTime.ofEpochSecond(fromDateTime(v.time).toLong(), (fromDateTime(v.time) % 1 * 1e9).toInt(), ZoneOffset.UTC) - .toString() - }", - LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC).toString(), + "${utcIso(t)}/${utcIso(fromDateTime(v.time))}", + utcIso(t), listOf(0.0, dt, dt, 0.0) ) ) tt.add( CZMLInterval( - "${ - LocalDateTime.ofEpochSecond(t.toLong(), ((t) % 1 * 1e9).toInt(), ZoneOffset.UTC) - }/${ - LocalDateTime.ofEpochSecond((fromDateTime(v.time)).toLong(), (fromDateTime(v.time) % 1 * 1e9).toInt(), ZoneOffset.UTC) - .toString() - }", - LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC).toString(), + "${utcIso(t)}/${utcIso(fromDateTime(v.time))}", + utcIso(t), listOf(0.0, 0.0, dt, dt) ) ) @@ -76,26 +71,16 @@ class CZMLPath( if (dt > 1) { lt.add( CZMLInterval( - "${ - LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC) - }/${ - LocalDateTime.ofEpochSecond((tk).toLong(), (tk % 1 * 1e9).toInt(), ZoneOffset.UTC) - .toString() - }", - LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC).toString(), + "${utcIso(t)}/${utcIso(tk)}", + utcIso(t), listOf(0.0, dt, dt, 0.0) ) ) tt.add( CZMLInterval( - "${ - LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC) - }/${ - LocalDateTime.ofEpochSecond((tk).toLong(), (tk % 1 * 1e9).toInt(), ZoneOffset.UTC) - .toString() - }", - LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC).toString(), + "${utcIso(t)}/${utcIso(tk)}", + utcIso(t), listOf(0.0, 0.0, dt, dt) ) ) diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/BallisticsService.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/BallisticsService.kt index cc64fbd..619c158 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/BallisticsService.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/BallisticsService.kt @@ -16,6 +16,7 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRevolutionsRequestDTO import space.nstart.pcp.slots_service.dto.tlecomparison.TleParsedDTO import space.nstart.pcp.slots_service.configuration.CustomErrorException +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.net.URI import java.time.LocalDateTime @@ -133,8 +134,8 @@ class BallisticsService (webClientBuilderProvider: ObjectProvider { - val tstart = timeStart ?: LocalDateTime.now() + val tstart = timeStart ?: UtcDateTimes.now() val tstop = timeStop ?: tstart.plusDays(1) var vuz = ballisticsService.ascNodes(id, tstart, tstop).collectList().block() vuz = vuz?.filter { it.time >= tstart && it.time <= tstop }?.sortedBy { it.time } @@ -489,7 +490,7 @@ private val docReqCovName ="req_cov_" cartographicDegrees = flr, interpolationAlgorithm = "LAGRANGE", interpolationDegree = 1.0, - epoch = tstart.toString() + epoch = UtcDateTimes.format(tstart) ), path = path.apply { show = showSw } @@ -500,7 +501,7 @@ private val docReqCovName ="req_cov_" cartographicDegrees = fll, interpolationAlgorithm = "LAGRANGE", interpolationDegree = 1.0, - epoch = tstart.toString() + epoch = UtcDateTimes.format(tstart) ), path = path.apply { show = showSw } ), @@ -521,7 +522,7 @@ private val docReqCovName ="req_cov_" cartographicDegrees = sw, interpolationAlgorithm = "LAGRANGE", interpolationDegree = 1.0, - epoch = tstart.toString() + epoch = UtcDateTimes.format(tstart) ), path = path.apply { show = showFl } ), @@ -550,7 +551,7 @@ private val docReqCovName ="req_cov_" cartesian = orb, interpolationAlgorithm = "LAGRANGE", interpolationDegree = 1.0, - epoch = tstart.toString() + epoch = UtcDateTimes.format(tstart) ), path = path.apply { show = showOrbit } ) diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/CoverageSchemeService.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/CoverageSchemeService.kt index fb77839..1ee649e 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/CoverageSchemeService.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/CoverageSchemeService.kt @@ -19,6 +19,7 @@ import space.nstart.pcp.slots_service.czml.CZMLMaterial import space.nstart.pcp.slots_service.czml.CZMLPolygon import space.nstart.pcp.slots_service.czml.CZMLPosition import tools.jackson.databind.ObjectMapper +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @Service @@ -185,8 +186,8 @@ class CoverageSchemeModeRowDTO( val entityId: String = "", val sequence: Int = 0, val satelliteId: Long = 0, - val tn: LocalDateTime = LocalDateTime.now(), - val tk: LocalDateTime = LocalDateTime.now(), + val tn: LocalDateTime = UtcDateTimes.now(), + val tk: LocalDateTime = UtcDateTimes.now(), val roll: Double? = null, val revolutionSign: RevolutionSign? = null, val contour: String = "", diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/DynamicPlanService.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/DynamicPlanService.kt index 46acd25..cc74cd8 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/DynamicPlanService.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/DynamicPlanService.kt @@ -8,11 +8,13 @@ import reactor.core.publisher.Mono import space.nstart.pcp.pcp_types_lib.dto.ballistics.AngularMotionCalculationRequestDTO import space.nstart.pcp.slots_service.configuration.CustomErrorException import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationRequestDTO +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import tools.jackson.databind.JsonNode import tools.jackson.databind.ObjectMapper import java.net.URI import java.net.URLEncoder import java.nio.charset.StandardCharsets +import java.time.LocalDateTime import java.util.UUID @Service @@ -112,10 +114,9 @@ class DynamicPlanService(webClientBuilderProvider: ObjectProvider value.isNotBlank() } - ?.let { value -> "?time=${URLEncoder.encode(value, StandardCharsets.UTF_8)}" } + ?.let { value -> "?time=${URLEncoder.encode(UtcDateTimes.format(value), StandardCharsets.UTF_8)}" } ?: "" return webClientBuilder.build() @@ -131,10 +132,10 @@ class DynamicPlanService(webClientBuilderProvider: ObjectProvider status.isError }) { response -> response.bodyToMono(String::class.java) diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/MissionService.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/MissionService.kt index f3fa13a..ad5caf6 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/MissionService.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/MissionService.kt @@ -11,6 +11,7 @@ import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.MissionResponseDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.MissionSurveyCalculationResponseDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.ModeResponseDTO import space.nstart.pcp.slots_service.dto.currentplans.CurrentPlansMissionPageDTO +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.util.UUID import java.time.LocalDateTime @@ -25,7 +26,7 @@ class MissionService(webClientBuilderProvider: ObjectProvider fun plan(sat : Long, tn : LocalDateTime, tk : LocalDateTime) = webClientBuilder.build() .get() - .uri("$url/api/missions/modes/satellite?satellite_id=$sat&timeStart=$tn&timeStop=$tk") + .uri("$url/api/missions/modes/satellite?satellite_id=$sat&timeStart=${UtcDateTimes.format(tn)}&timeStop=${UtcDateTimes.format(tk)}") .retrieve() .bodyToFlux() diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SatellitePdcmService.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SatellitePdcmService.kt index acf13a5..c937159 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SatellitePdcmService.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SatellitePdcmService.kt @@ -6,6 +6,7 @@ import space.nstart.pcp.slots_service.configuration.CustomErrorException import space.nstart.pcp.slots_service.dto.pdcm.SatellitePdcmAvailabilityDTO import space.nstart.pcp.slots_service.dto.pdcm.SatellitePdcmDetailsDTO import space.nstart.pcp.slots_service.dto.pdcm.SatellitePdcmSummaryDTO +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @Service @@ -14,7 +15,7 @@ class SatellitePdcmService( private val ballisticsService: BallisticsService ) { - fun satelliteSummaries(): List = satelliteSummaries(LocalDateTime.now()) + fun satelliteSummaries(): List = satelliteSummaries(UtcDateTimes.now()) fun satelliteSummaries(referenceTime: LocalDateTime): List { val satellites = satelliteCatalogService.allSatellites() @@ -39,7 +40,7 @@ class SatellitePdcmService( } fun satelliteDetails(satelliteId: Long): SatellitePdcmDetailsDTO = - satelliteDetails(satelliteId, LocalDateTime.now()) + satelliteDetails(satelliteId, UtcDateTimes.now()) fun satelliteDetails(satelliteId: Long, referenceTime: LocalDateTime): SatellitePdcmDetailsDTO { val satellites = satelliteCatalogService.allSatellites() diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt index 18a6ce6..e2c5379 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt @@ -23,6 +23,7 @@ import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO import tools.jackson.databind.ObjectMapper import space.nstart.pcp.slots_service.configuration.CustomErrorException import space.nstart.pcp.pcp_types_lib.dto.BookedSlotDetailsDTO +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @Service @@ -47,8 +48,8 @@ class SlotService(webClientBuilderProvider: ObjectProvider) { UriComponentsBuilder .fromUriString("$url/api/slots/request-cover") .queryParam("requestId", id) - .queryParam("timeStart", tn) - .queryParam("timeStop", tk) + .queryParam("timeStart", UtcDateTimes.format(tn)) + .queryParam("timeStop", UtcDateTimes.format(tk)) .queryParamIfPresent("revSign", java.util.Optional.ofNullable(sign)) .queryParam("cov", cov) .queryParamIfPresent("sun", java.util.Optional.ofNullable(sun)) @@ -70,7 +71,7 @@ class SlotService(webClientBuilderProvider: ObjectProvider) { fun plan(id : Long, tn : LocalDateTime, tk : LocalDateTime) = webClientBuilder.build() .get() - .uri("$url/api/slots/mission/satellite?satellite_id=$id&timeStart=$tn&timeStop=$tk") + .uri("$url/api/slots/mission/satellite?satellite_id=$id&timeStart=${UtcDateTimes.format(tn)}&timeStop=${UtcDateTimes.format(tk)}") .retrieve() .onStatus({ status -> status.isError }) { response -> response.bodyToMono(String::class.java) @@ -82,7 +83,7 @@ class SlotService(webClientBuilderProvider: ObjectProvider) { fun allSlotsByInterval(id: Long, tn: LocalDateTime, tk: LocalDateTime): List = webClientBuilder.build() .get() - .uri("$url/api/slots/interval?satelliteId=$id&timeStart=$tn&timeStop=$tk") + .uri("$url/api/slots/interval?satelliteId=$id&timeStart=${UtcDateTimes.format(tn)}&timeStop=${UtcDateTimes.format(tk)}") .retrieve() .onStatus({ status -> status.isError }) { response -> response.bodyToMono(String::class.java) @@ -193,8 +194,8 @@ class SlotService(webClientBuilderProvider: ObjectProvider) { .uri( UriComponentsBuilder .fromUriString("$url/api/slots/booking/search") - .queryParamIfPresent("timeStart", java.util.Optional.ofNullable(timeStart)) - .queryParamIfPresent("timeStop", java.util.Optional.ofNullable(timeStop)) + .queryParamIfPresent("timeStart", java.util.Optional.ofNullable(timeStart?.let { UtcDateTimes.format(it) })) + .queryParamIfPresent("timeStop", java.util.Optional.ofNullable(timeStop?.let { UtcDateTimes.format(it) })) .queryParamIfPresent("requestId", java.util.Optional.ofNullable(requestId?.trim()?.takeIf { it.isNotEmpty() })) .apply { satelliteIds.orEmpty().distinct().forEach { queryParam("satelliteIds", it) } @@ -331,7 +332,7 @@ class SlotService(webClientBuilderProvider: ObjectProvider) { .uri( UriComponentsBuilder .fromUriString("$url/api/satellite/{satelliteId}/send-ic") - .queryParam("time", time) + .queryParam("time", UtcDateTimes.format(time)) .buildAndExpand(satelliteId) .toUriString() ) diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/StationService.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/StationService.kt index e437b07..c25cdd0 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/StationService.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/StationService.kt @@ -2,6 +2,7 @@ package space.nstart.pcp.slots_service.service import org.springframework.beans.factory.ObjectProvider import org.springframework.beans.factory.annotation.Value +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import org.springframework.web.reactive.function.client.WebClient import reactor.core.publisher.Mono @@ -11,6 +12,7 @@ import tools.jackson.databind.ObjectMapper @Service class StationService(webClientBuilderProvider: ObjectProvider) { + private val log = LoggerFactory.getLogger(StationService::class.java) private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder() @Value("\${settings.stations-service:stations-service}") @@ -36,6 +38,13 @@ class StationService(webClientBuilderProvider: ObjectProvider fun allStations(): List = stations().collectList().block() ?: emptyList() + fun allStationsOrEmpty(): List = + runCatching { allStations() } + .onFailure { error -> + log.warn("Stations service is unavailable. Map page will be opened without stations.", error) + } + .getOrDefault(emptyList()) + fun saveStation(request: StationDTO): StationDTO = webClientBuilder.build() .post() diff --git a/services/pcp-ui-service/src/main/resources/static/angular_motion_scripts.js b/services/pcp-ui-service/src/main/resources/static/angular_motion_scripts.js index b6d348a..b1fa610 100644 --- a/services/pcp-ui-service/src/main/resources/static/angular_motion_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/angular_motion_scripts.js @@ -53,26 +53,24 @@ function nowLocalInput() { const d = new Date(); d.setSeconds(0, 0); - const offsetMs = d.getTimezoneOffset() * 60000; - return new Date(d.getTime() - offsetMs).toISOString().slice(0, 16); + return PcpUtcTime.toLocalInputValue(d); } function localDateTimeValue() { - const value = el('angular-motion-time').value; - return value && value.length === 16 ? `${value}:00` : value; + return PcpUtcTime.toUtcIsoFromInput(el('angular-motion-time').value); } function toDateTimeInputValue(value) { - return text(value).slice(0, 16); + return PcpUtcTime.toLocalInputValue(value); } - function normalizeDateTimeSeconds(value) { - const textValue = text(value); - return textValue && textValue.length === 16 ? `${textValue}:00` : textValue; + function dateTimeMillis(value) { + const date = PcpUtcTime.parseApiDate(value); + return date ? date.getTime() : NaN; } function formatDateTime(value) { - return text(value, '-').replace('T', ' '); + return PcpUtcTime.formatDateTime(value); } function formatNumber(value, digits = 6) { @@ -119,11 +117,11 @@ const sat = state.selectedSatellite; if (!sat?.availabilityStart || !sat?.availabilityStop) return; - const current = normalizeDateTimeSeconds(localDateTimeValue()); - const start = normalizeDateTimeSeconds(sat.availabilityStart); - const stop = normalizeDateTimeSeconds(sat.availabilityStop); - if (!current || current < start || current > stop) { - el('angular-motion-time').value = toDateTimeInputValue(start); + const current = dateTimeMillis(localDateTimeValue()); + const start = dateTimeMillis(sat.availabilityStart); + const stop = dateTimeMillis(sat.availabilityStop); + if (!Number.isFinite(current) || current < start || current > stop) { + el('angular-motion-time').value = toDateTimeInputValue(sat.availabilityStart); } } diff --git a/services/pcp-ui-service/src/main/resources/static/bookings_scripts.js b/services/pcp-ui-service/src/main/resources/static/bookings_scripts.js index c21d5fd..c2ac9db 100644 --- a/services/pcp-ui-service/src/main/resources/static/bookings_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/bookings_scripts.js @@ -63,8 +63,8 @@ const timeStop = el('bookings-time-stop')?.value; const requestId = el('bookings-request')?.value; - if (timeStart) params.set('timeStart', toIsoLocal(timeStart)); - if (timeStop) params.set('timeStop', toIsoLocal(timeStop)); + if (timeStart) params.set('timeStart', PcpUtcTime.toUtcIsoFromInput(timeStart)); + if (timeStop) params.set('timeStop', PcpUtcTime.toUtcIsoFromInput(timeStop)); if (requestId) params.set('requestId', requestId); state.selectedSatelliteIds.forEach((id) => params.append('satelliteIds', id)); @@ -240,13 +240,7 @@ } function formatDateTime(value) { - if (!value) return '—'; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return escapeHtml(value); - return date.toLocaleString('ru-RU', { - year: '2-digit', month: '2-digit', day: '2-digit', - hour: '2-digit', minute: '2-digit', second: '2-digit' - }); + return value ? PcpUtcTime.formatDateTime(value) : '—'; } function formatNumber(value) { @@ -257,12 +251,7 @@ } function toDateTimeLocal(date) { - const pad = (value) => String(value).padStart(2, '0'); - return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; - } - - function toIsoLocal(value) { - return value && value.length === 16 ? `${value}:00` : value; + return PcpUtcTime.toLocalInputValue(date); } function showAlert(message, type = 'info') { diff --git a/services/pcp-ui-service/src/main/resources/static/catalog_scripts.js b/services/pcp-ui-service/src/main/resources/static/catalog_scripts.js index 26e668b..af24c5e 100644 --- a/services/pcp-ui-service/src/main/resources/static/catalog_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/catalog_scripts.js @@ -50,14 +50,10 @@ } function formatDateTimeInput(value) { - if (!value) return ''; - const match = String(value).match( - /^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?/ - ); - if (!match) return value; - - const [, year, month, day, hour, minute, second = '00', millis = '000'] = match; - return `${day}.${month}.${year} ${hour}:${minute}:${second}.${millis.slice(0, 3).padEnd(3, '0')}`; + const date = PcpUtcTime.parseApiDate(value); + if (!date) return value || ''; + const pad = (number, length = 2) => String(number).padStart(length, '0'); + return `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`; } function parseDateTimeInput(value, fieldName, required = false) { @@ -73,14 +69,14 @@ if (displayMatch) { const [, day, month, year, hour, minute, second, millis] = displayMatch; validateDateTimeParts(day, month, year, hour, minute, second, fieldName); - return `${year}-${month}-${day}T${hour}:${minute}:${second}.${millis.padEnd(3, '0')}`; + return PcpUtcTime.toUtcIsoFromRussianDateTime(`${day}.${month}.${year} ${hour}:${minute}:${second}.${millis.padEnd(3, '0')}`); } const isoMatch = text.match(/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,3}))?)?$/); if (isoMatch) { const [, year, month, day, hour, minute, second = '00', millis = '000'] = isoMatch; validateDateTimeParts(day, month, year, hour, minute, second, fieldName); - return `${year}-${month}-${day}T${hour}:${minute}:${second}.${millis.padEnd(3, '0')}`; + return PcpUtcTime.toUtcIsoFromRussianDateTime(`${day}.${month}.${year} ${hour}:${minute}:${second}.${millis.padEnd(3, '0')}`); } throw new Error(`${fieldName}: укажите время в формате dd.mm.yyyy hh:mm:ss.zzz`); diff --git a/services/pcp-ui-service/src/main/resources/static/cesium_scripts.js b/services/pcp-ui-service/src/main/resources/static/cesium_scripts.js index 79d4fb8..8bf0d16 100644 --- a/services/pcp-ui-service/src/main/resources/static/cesium_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/cesium_scripts.js @@ -19,45 +19,54 @@ } - /** получение строки декретного московского времени - */ - var TDMT = function GetDMTime(e,t) + function localDateFromJulianDate(julianDate) { - var d = new Cesium.JulianDate(); - Cesium.JulianDate.addHours(e,3,d) - var gregorian = new Cesium.JulianDate.toGregorianDate(d); - //var s = new String(); - if (Math.abs(t._clockViewModel.multiplier)<1) - s = STRTOFORMAT(gregorian.hour,2) + ':' + STRTOFORMAT(gregorian.minute,2) + ':' + STRTOFORMAT(gregorian.second,2)+'.'+STRTOFORMAT(Math.round(gregorian.millisecond)) - else - s = STRTOFORMAT(gregorian.hour,2) + ':' + STRTOFORMAT(gregorian.minute,2) + ':' + STRTOFORMAT(gregorian.second,2)+' ДМВ'; - - return s; + return Cesium.JulianDate.toDate(julianDate); } - /** получение строки декретной московской даты - */ - var TDMD = function GetDMDate(e) + function localTimeZoneLabel() { - var d = new Cesium.JulianDate(); - Cesium.JulianDate.addHours(e,3,d) - var gregorian = new Cesium.JulianDate.toGregorianDate(d); - - s = STRTOFORMAT(gregorian.day,2)+ ' ' + MNTH[gregorian.month-1] + ' ' + STRTOFORMAT(gregorian.year,4); - - return s; + try { + var parts = new Intl.DateTimeFormat('ru-RU', {timeZoneName: 'short'}).formatToParts(new Date()); + var part = parts.find(function (item) { return item.type === 'timeZoneName'; }); + return part && part.value ? part.value : 'лок.'; + } catch (error) { + return 'лок.'; + } } - /** получение строки декретного московского даты и времени + /** получение строки времени в часовом поясе браузера */ - var TDMDT = function GetDMDdateTime(e) + var TDMT = function GetLocalTime(e,t) + { + var date = localDateFromJulianDate(e); + if (Math.abs(t._clockViewModel.multiplier)<1) + s = STRTOFORMAT(date.getHours(),2) + ':' + STRTOFORMAT(date.getMinutes(),2) + ':' + STRTOFORMAT(date.getSeconds(),2)+'.'+STRTOFORMAT(date.getMilliseconds(),3) + else + s = STRTOFORMAT(date.getHours(),2) + ':' + STRTOFORMAT(date.getMinutes(),2) + ':' + STRTOFORMAT(date.getSeconds(),2)+' '+localTimeZoneLabel(); + + return s; + } + + /** получение строки даты в часовом поясе браузера + */ + var TDMD = function GetLocalDate(e) + { + var date = localDateFromJulianDate(e); + + s = STRTOFORMAT(date.getDate(),2)+ ' ' + MNTH[date.getMonth()] + ' ' + STRTOFORMAT(date.getFullYear(),4); + + return s; + } + + /** получение строки даты и времени в часовом поясе браузера + */ + var TDMDT = function GetLocalDateTime(e) { s = ""; - var d = new Cesium.JulianDate(); - Cesium.JulianDate.addHours(e,3,d); - var gregorian = new Cesium.JulianDate.toGregorianDate(d); - s = gregorian.day+" "+ MNTH[gregorian.month-1]+" "+ gregorian.year+" "+STRTOFORMAT(gregorian.hour,2)+":"+STRTOFORMAT(gregorian.minute,2)+":"+STRTOFORMAT(gregorian.second,2)+" ДМВ"; - return s; + var date = localDateFromJulianDate(e); + s = date.getDate()+" "+ MNTH[date.getMonth()]+" "+date.getFullYear()+" "+STRTOFORMAT(date.getHours(),2)+":"+STRTOFORMAT(date.getMinutes(),2)+":"+STRTOFORMAT(date.getSeconds(),2)+" "+localTimeZoneLabel(); + return s; } diff --git a/services/pcp-ui-service/src/main/resources/static/complex_plan_scripts.js b/services/pcp-ui-service/src/main/resources/static/complex_plan_scripts.js index 7493947..3a0fcb0 100644 --- a/services/pcp-ui-service/src/main/resources/static/complex_plan_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/complex_plan_scripts.js @@ -34,12 +34,7 @@ } function formatDateTime(value) { - if (!value) return '-'; - const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return value; - } - return date.toLocaleString('ru-RU'); + return value ? PcpUtcTime.formatDateTime(value) : '-'; } function formatDuration(value) { @@ -70,23 +65,12 @@ function isoDateTime(value) { if (!value) return ''; - const date = new Date(value); - return Number.isNaN(date.getTime()) ? String(value) : date.toISOString(); + const date = PcpUtcTime.parseApiDate(value); + return date ? date.toISOString() : String(value); } function formatDateTimeLocal(value) { - if (!value) return ''; - const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return String(value).slice(0, 16); - } - - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - return `${year}-${month}-${day}T${hours}:${minutes}`; + return PcpUtcTime.toLocalInputValue(value); } async function requestJson(url, options) { @@ -464,8 +448,8 @@ 'Content-Type': 'application/json' }, body: JSON.stringify({ - intervalStart: intervalStart, - intervalEnd: intervalEnd, + intervalStart: PcpUtcTime.toUtcIsoFromInput(intervalStart), + intervalEnd: PcpUtcTime.toUtcIsoFromInput(intervalEnd), satelliteId: null, satelliteIds: satelliteIds, sun: sunValue ? Number(sunValue) : null, @@ -565,9 +549,7 @@ } function parseDate(value) { - if (!value) return null; - const date = new Date(value); - return Number.isNaN(date.getTime()) ? null : date; + return PcpUtcTime.parseApiDate(value); } function intervalDurationDays(run) { diff --git a/services/pcp-ui-service/src/main/resources/static/css/angular-motion.css b/services/pcp-ui-service/src/main/resources/static/css/angular-motion.css index f0e5d3c..333010b 100644 --- a/services/pcp-ui-service/src/main/resources/static/css/angular-motion.css +++ b/services/pcp-ui-service/src/main/resources/static/css/angular-motion.css @@ -1,12 +1,27 @@ .angular-motion-page { height: 100%; + min-height: 0; + display: flex; + flex-direction: column; } .angular-motion-card { + min-height: 0; border: 1px solid #dbe4f0; box-shadow: 0 0.25rem 0.75rem rgba(15, 23, 42, 0.05); } +.angular-motion-layout { + flex: 1 1 auto; + min-height: 0; +} + +.angular-motion-layout > [class*="col-"] { + min-height: 0; + display: flex; + flex-direction: column; +} + .angular-motion-section-title { font-weight: 600; color: #1f2a44; @@ -18,8 +33,7 @@ } .angular-motion-form-body { - max-height: calc(100vh - 11rem); - overflow: auto; + overflow: visible; } .angular-motion-map { @@ -123,8 +137,16 @@ } @media (max-width: 1199.98px) { + .angular-motion-page { + height: auto; + } + + .angular-motion-layout > [class*="col-"] { + display: block; + } + .angular-motion-form-body { - max-height: none; + overflow: visible; } .angular-motion-table-wrap { diff --git a/services/pcp-ui-service/src/main/resources/static/current_plans_scripts.js b/services/pcp-ui-service/src/main/resources/static/current_plans_scripts.js index 962ed3b..a5c22bf 100644 --- a/services/pcp-ui-service/src/main/resources/static/current_plans_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/current_plans_scripts.js @@ -475,8 +475,8 @@ const body = { satelliteId: Number(state.selectedSatelliteId), station: trimToNull(el('current-plans-create-station')?.value), - missionStart, - missionStop, + missionStart: PcpUtcTime.toUtcIsoFromInput(missionStart), + missionStop: PcpUtcTime.toUtcIsoFromInput(missionStop), status: trimToNull(el('current-plans-create-status')?.value) || 'NEW' }; @@ -741,23 +741,7 @@ } function formatDateTime(value) { - if (!value) return '—'; - const match = String(value).match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?/); - if (match) { - const [, year, month, day, hour, minute, second] = match; - return `${day}.${month}.${year.slice(2)}, ${hour}:${minute}:${second || '00'}`; - } - - const date = new Date(value); - if (Number.isNaN(date.getTime())) return escapeHtml(value); - return date.toLocaleString('ru-RU', { - year: '2-digit', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); + return value ? PcpUtcTime.formatDateTime(value) : '—'; } function formatNumber(value) { @@ -782,8 +766,7 @@ } function toDateTimeLocalValue(date) { - const pad = (number) => String(number).padStart(2, '0'); - return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; + return PcpUtcTime.toLocalInputValue(date); } function trimToNull(value) { diff --git a/services/pcp-ui-service/src/main/resources/static/dynamic_plan_map_layers.js b/services/pcp-ui-service/src/main/resources/static/dynamic_plan_map_layers.js index 43456cf..5c28f50 100644 --- a/services/pcp-ui-service/src/main/resources/static/dynamic_plan_map_layers.js +++ b/services/pcp-ui-service/src/main/resources/static/dynamic_plan_map_layers.js @@ -124,7 +124,11 @@ } function formatValue(value) { - return value === null || value === undefined || value === '' ? '-' : String(value).replace('T', ' '); + return value === null || value === undefined || value === '' ? '-' : String(value); + } + + function formatDateValue(value) { + return value === null || value === undefined || value === '' ? '-' : PcpUtcTime.formatDateTime(value); } function createRoutesDataSource(run, routes, intervals) { @@ -178,8 +182,8 @@ `Run ID: ${formatValue(run.runId)}`, `Request ID: ${formatValue(route.requestId || run.requestId)}`, `Satellite: ${formatValue(route.satelliteId)}`, - `Start: ${formatValue(route.startTime)}`, - `End: ${formatValue(route.endTime)}`, + `Start: ${formatDateValue(route.startTime)}`, + `End: ${formatDateValue(route.endTime)}`, `Duration: ${formatValue(route.duration)}`, `Revolution: ${formatValue(route.revolution)}`, `Roll: ${formatValue(route.roll)}` diff --git a/services/pcp-ui-service/src/main/resources/static/dynamic_plan_scripts.js b/services/pcp-ui-service/src/main/resources/static/dynamic_plan_scripts.js index ef8dbfc..228eab2 100644 --- a/services/pcp-ui-service/src/main/resources/static/dynamic_plan_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/dynamic_plan_scripts.js @@ -51,7 +51,7 @@ } function formatDateTime(value) { - return text(value, '-').replace('T', ' '); + return value ? PcpUtcTime.formatDateTime(value) : '-'; } function startOfDay(value) { diff --git a/services/pcp-ui-service/src/main/resources/static/group_state_scripts.js b/services/pcp-ui-service/src/main/resources/static/group_state_scripts.js index fb1f3aa..8b61b4b 100644 --- a/services/pcp-ui-service/src/main/resources/static/group_state_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/group_state_scripts.js @@ -51,14 +51,11 @@ } function formatDateTimeLabel(value) { - return value ? value.replace('T', ' ') : '-'; + return value ? PcpUtcTime.formatDateTime(value) : '-'; } function formatInputDateTime(value) { - if (!value) { - return ''; - } - return value.length >= 19 ? value.slice(0, 19) : value; + return PcpUtcTime.toLocalInputValue(value, true); } function formatAngle(value) { @@ -273,7 +270,7 @@ state.selectedGroupId = groupId; renderGroupList(); - const query = requestedTime ? `?time=${encodeURIComponent(requestedTime)}` : ''; + const query = requestedTime ? `?time=${encodeURIComponent(PcpUtcTime.toUtcIsoFromInput(requestedTime))}` : ''; const details = await requestJson(`${apiBase}/${groupId}${query}`); renderDetails(details); } diff --git a/services/pcp-ui-service/src/main/resources/static/requests_scripts.js b/services/pcp-ui-service/src/main/resources/static/requests_scripts.js index 0fa3bf7..2a0bf80 100644 --- a/services/pcp-ui-service/src/main/resources/static/requests_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/requests_scripts.js @@ -971,13 +971,13 @@ function renderSlotsTable() { return (aValue - bValue) * direction; case 'tn': - aValue = new Date(a.tn).getTime(); - bValue = new Date(b.tn).getTime(); + aValue = PcpUtcTime.parseApiDate(a.tn)?.getTime() ?? 0; + bValue = PcpUtcTime.parseApiDate(b.tn)?.getTime() ?? 0; return (aValue - bValue) * direction; case 'tk': - aValue = new Date(a.tk).getTime(); - bValue = new Date(b.tk).getTime(); + aValue = PcpUtcTime.parseApiDate(a.tk)?.getTime() ?? 0; + bValue = PcpUtcTime.parseApiDate(b.tk)?.getTime() ?? 0; return (aValue - bValue) * direction; case 'duration': @@ -1078,29 +1078,23 @@ function createSlotRowHTML(slot, index) { // Форматирование даты и времени function formatDateTime(dateTimeString) { if (!dateTimeString) return '-'; - - try { - const date = new Date(dateTimeString); - return date.toLocaleString('ru-RU', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); - } catch (e) { - console.error('Ошибка форматирования даты:', e); - return dateTimeString; - } + return PcpUtcTime.formatDateTime(dateTimeString, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); } // Расчет длительности в минутах function calculateDuration(tn, tk) { if (!tn || !tk) return 0; - const startDate = new Date(tn); - const endDate = new Date(tk); + const startDate = PcpUtcTime.parseApiDate(tn); + const endDate = PcpUtcTime.parseApiDate(tk); + if (!startDate || !endDate) return 0; return (endDate - startDate) / (1000 * 60); // в минутах } @@ -1206,12 +1200,14 @@ function updateSlotsSummary() { ).size; // Период покрытия - const startTimes = currentSlotsData.map(slot => new Date(slot.tn)); - const endTimes = currentSlotsData.map(slot => new Date(slot.tk)); - const earliest = new Date(Math.min(...startTimes)); - const latest = new Date(Math.max(...endTimes)); + const startTimes = currentSlotsData.map(slot => PcpUtcTime.parseApiDate(slot.tn)).filter(Boolean); + const endTimes = currentSlotsData.map(slot => PcpUtcTime.parseApiDate(slot.tk)).filter(Boolean); + const earliest = startTimes.length ? new Date(Math.min(...startTimes)) : null; + const latest = endTimes.length ? new Date(Math.max(...endTimes)) : null; - const period = `${earliest.toLocaleDateString('ru-RU')} - ${latest.toLocaleDateString('ru-RU')}`; + const period = earliest && latest + ? `${earliest.toLocaleDateString('ru-RU')} - ${latest.toLocaleDateString('ru-RU')}` + : '-'; // Обновляем элементы document.getElementById('totalSlotsCount').textContent = totalSlots; diff --git a/services/pcp-ui-service/src/main/resources/static/rva_scripts.js b/services/pcp-ui-service/src/main/resources/static/rva_scripts.js index 21ea05c..f7c061b 100644 --- a/services/pcp-ui-service/src/main/resources/static/rva_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/rva_scripts.js @@ -22,12 +22,7 @@ } function formatDateTime(value) { - if (!value) return ''; - const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return value; - } - return date.toLocaleString('ru-RU'); + return value ? PcpUtcTime.formatDateTime(value) : ''; } function formatNumber(value, digits = 3) { diff --git a/services/pcp-ui-service/src/main/resources/static/satellite_pdcm_scripts.js b/services/pcp-ui-service/src/main/resources/static/satellite_pdcm_scripts.js index 6bcbf02..46d7de7 100644 --- a/services/pcp-ui-service/src/main/resources/static/satellite_pdcm_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/satellite_pdcm_scripts.js @@ -56,7 +56,7 @@ } function formatDateTimeLabel(value) { - return value ? value.replace('T', ' ') : '-'; + return value ? PcpUtcTime.formatDateTime(value) : '-'; } function pad(value, length = 2) { @@ -69,18 +69,11 @@ } function formatDateTimeInput(value) { - if (!value) { - return ''; + const date = PcpUtcTime.parseApiDate(value); + if (!date) { + return value || ''; } - const match = String(value).match( - /^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?/ - ); - if (!match) { - return value; - } - - const [, year, month, day, hour, minute, second = '00', millis = '000'] = match; - return `${day}.${month}.${year} ${hour}:${minute}:${second}.${millis.slice(0, 3).padEnd(3, '0')}`; + return `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`; } function parseSendInitialConditionsTime() { @@ -121,7 +114,8 @@ return { valid: true, - text: value + text: value, + iso: PcpUtcTime.toUtcIsoFromRussianDateTime(value) }; } @@ -154,7 +148,7 @@ throw new Error(`${fieldName}: некорректная дата или время`); } - return `${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}`; + return PcpUtcTime.toUtcIsoFromRussianDateTime(text); } function parseRequiredNumber(inputId, fieldName) { @@ -436,7 +430,7 @@ renderSendInitialConditionsState(state.details, true); showAlert(''); try { - await requestJson(`${apiBase}/${state.selectedSatelliteId}/send-ic?time=${encodeURIComponent(time.text)}`, {method: 'POST'}); + await requestJson(`${apiBase}/${state.selectedSatelliteId}/send-ic?time=${encodeURIComponent(time.iso)}`, {method: 'POST'}); showAlert('Начальные условия спутника переданы в slot-service', 'success'); } catch (error) { showAlert(error.message || 'Не удалось передать начальные условия в slot-service'); diff --git a/services/pcp-ui-service/src/main/resources/static/tgu_planning_scripts.js b/services/pcp-ui-service/src/main/resources/static/tgu_planning_scripts.js index eb64ffa..0d6d78b 100644 --- a/services/pcp-ui-service/src/main/resources/static/tgu_planning_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/tgu_planning_scripts.js @@ -480,9 +480,7 @@ } function parsePlanDate(value) { - if (!value) return null; - const date = new Date(value); - return Number.isNaN(date.getTime()) ? null : date; + return PcpUtcTime.parseApiDate(value); } function startOfToday() { @@ -491,8 +489,7 @@ } function toDatetimeLocalValue(date) { - const pad = (value) => String(value).padStart(2, '0'); - return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; + return PcpUtcTime.toLocalInputValue(date); } function timelineTicks(from, to) { @@ -632,9 +629,7 @@ function formatDateTime(value) { if (!value) return '—'; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return escapeHtml(value); - return date.toLocaleString('ru-RU', { + const formatted = PcpUtcTime.formatDateTime(value, { year: '2-digit', month: '2-digit', day: '2-digit', @@ -642,6 +637,7 @@ minute: '2-digit', second: '2-digit' }); + return formatted === '—' ? escapeHtml(value) : formatted; } function formatAxisTick(value) { diff --git a/services/pcp-ui-service/src/main/resources/static/tle_analysis_scripts.js b/services/pcp-ui-service/src/main/resources/static/tle_analysis_scripts.js index 47c5eb9..6708d2f 100644 --- a/services/pcp-ui-service/src/main/resources/static/tle_analysis_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/tle_analysis_scripts.js @@ -235,8 +235,7 @@ } function formatDateTime(value) { - if (!value) return '—'; - return String(value).replace('T', ' '); + return value ? PcpUtcTime.formatDateTime(value) : '—'; } function formatNumber(value, fractionDigits) { diff --git a/services/pcp-ui-service/src/main/resources/static/tle_comparison_scripts.js b/services/pcp-ui-service/src/main/resources/static/tle_comparison_scripts.js index 8fdb0a2..1de76fa 100644 --- a/services/pcp-ui-service/src/main/resources/static/tle_comparison_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/tle_comparison_scripts.js @@ -64,7 +64,9 @@ return; } const minTime = maxEpoch(); - if (minTime && time < minTime) { + const calculationDate = parseCalculationInput(); + const minDate = PcpUtcTime.parseApiDate(minTime); + if (calculationDate && minDate && calculationDate.getTime() < minDate.getTime()) { showAlert(`Время расчета должно быть не раньше максимальной эпохи TLE: ${formatDateTime(minTime)}.`, 'warning'); return; } @@ -145,8 +147,9 @@ const input = el('tle-comparison-time'); const minTime = maxEpoch(); if (!input || !minTime || !input.value) return; - const time = getCalculationTime(); - if (time && time < minTime) { + const time = parseCalculationInput(); + const minDate = PcpUtcTime.parseApiDate(minTime); + if (time && minDate && time.getTime() < minDate.getTime()) { showAlert(`Время расчета должно быть не раньше максимальной эпохи TLE: ${formatDateTime(minTime)}.`, 'warning'); } } @@ -186,13 +189,20 @@ } function maxEpoch() { - if (!state.first?.time || !state.second?.time) return null; - return state.first.time >= state.second.time ? state.first.time : state.second.time; + const first = PcpUtcTime.parseApiDate(state.first?.time); + const second = PcpUtcTime.parseApiDate(state.second?.time); + if (!first || !second) return null; + return first.getTime() >= second.getTime() ? state.first.time : state.second.time; + } + + function parseCalculationInput() { + const value = el('tle-comparison-time')?.value; + return value ? new Date(value) : null; } function getCalculationTime() { const value = el('tle-comparison-time')?.value; - return value ? value : null; + return value ? PcpUtcTime.toUtcIsoFromInput(value) : null; } function getTleText(side) { @@ -225,13 +235,11 @@ } function toDateTimeLocalValue(value) { - if (!value) return ''; - return String(value).slice(0, 19); + return PcpUtcTime.toLocalInputValue(value, true); } function formatDateTime(value) { - if (!value) return ''; - return String(value).replace('T', ' '); + return value ? PcpUtcTime.formatDateTime(value) : ''; } function formatNumber(value) { diff --git a/services/pcp-ui-service/src/main/resources/static/utc_time.js b/services/pcp-ui-service/src/main/resources/static/utc_time.js new file mode 100644 index 0000000..983ec66 --- /dev/null +++ b/services/pcp-ui-service/src/main/resources/static/utc_time.js @@ -0,0 +1,72 @@ +(function () { + function parseApiDate(value) { + if (!value) return null; + const text = String(value).trim(); + if (!text) return null; + const isoLike = /^\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}/.test(text); + const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/.test(text); + const normalized = isoLike && !hasZone ? `${text.replace(' ', 'T')}Z` : text; + const date = new Date(normalized); + return Number.isNaN(date.getTime()) ? null : date; + } + + function pad(value, length = 2) { + return String(value).padStart(length, '0'); + } + + function toLocalInputValue(value, includeSeconds = false) { + const date = value instanceof Date ? value : parseApiDate(value); + if (!date) return ''; + const base = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; + return includeSeconds ? `${base}:${pad(date.getSeconds())}` : base; + } + + function toUtcIsoFromInput(value) { + if (!value) return ''; + const date = new Date(value); + return Number.isNaN(date.getTime()) ? '' : date.toISOString(); + } + + function toUtcIsoFromRussianDateTime(value) { + const match = String(value || '').trim().match(/^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2}):(\d{2})\.(\d{1,3})$/); + if (!match) return ''; + const [, day, month, year, hour, minute, second, millisecond] = match; + const date = new Date( + Number(year), + Number(month) - 1, + Number(day), + Number(hour), + Number(minute), + Number(second), + Number(millisecond.padEnd(3, '0')) + ); + const valid = date.getFullYear() === Number(year) + && date.getMonth() === Number(month) - 1 + && date.getDate() === Number(day) + && date.getHours() === Number(hour) + && date.getMinutes() === Number(minute) + && date.getSeconds() === Number(second); + return valid ? date.toISOString() : ''; + } + + function formatDateTime(value, options = {}) { + const date = value instanceof Date ? value : parseApiDate(value); + if (!date) return options.fallback || '-'; + return date.toLocaleString(options.locale || 'ru-RU', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: options.seconds === false ? undefined : '2-digit' + }); + } + + window.PcpUtcTime = { + parseApiDate, + toLocalInputValue, + toUtcIsoFromInput, + toUtcIsoFromRussianDateTime, + formatDateTime + }; +})(); diff --git a/services/pcp-ui-service/src/main/resources/templates/fragments/base/general.html b/services/pcp-ui-service/src/main/resources/templates/fragments/base/general.html index b367d44..0ea2a4a 100644 --- a/services/pcp-ui-service/src/main/resources/templates/fragments/base/general.html +++ b/services/pcp-ui-service/src/main/resources/templates/fragments/base/general.html @@ -1,10 +1,11 @@ - - + + + diff --git a/services/pcp-ui-service/src/main/resources/templates/map.html b/services/pcp-ui-service/src/main/resources/templates/map.html index 1193fe8..6f797bb 100644 --- a/services/pcp-ui-service/src/main/resources/templates/map.html +++ b/services/pcp-ui-service/src/main/resources/templates/map.html @@ -1268,23 +1268,22 @@ } - // Функция для преобразования datetime-local в ISO + // Преобразует локальное datetime-local браузера в UTC ISO для API function toISOString(datetimeLocal) { if (!datetimeLocal) { - return null; // или можно вернуть текущее время по умолчанию + return null; } - // datetime-local возвращает строку в формате "YYYY-MM-DDTHH:mm" - // Создаем Date объект из этой строки - let date = new Date(datetimeLocal); - date.setHours(date.getHours() + 3); + const date = new Date(datetimeLocal); + if (Number.isNaN(date.getTime())) { + throw new Error('Некорректный формат даты'); + } - // Возвращаем в ISO формате - return date.toISOString().split('Z')[0]; + return date.toISOString(); } - - - + + + function covToReq(show, req_id){ // Сбрасываем чекбокс видимости всех слотов diff --git a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/configuration/UtcTimeConfiguration.kt b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/configuration/UtcTimeConfiguration.kt new file mode 100644 index 0000000..7e44bc0 --- /dev/null +++ b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/configuration/UtcTimeConfiguration.kt @@ -0,0 +1,55 @@ +package space.nstart.pcp.slots_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 slotsUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder -> + builder.addModule( + SimpleModule("slots-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 { + override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source) +} + +private class UtcLocalDateTimeSerializer : StdSerializer(LocalDateTime::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class UtcLocalDateTimeDeserializer : StdDeserializer(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) + } +} diff --git a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/BookingController.kt b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/BookingController.kt index b30740f..f734257 100644 --- a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/BookingController.kt +++ b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/BookingController.kt @@ -1,7 +1,6 @@ package space.nstart.pcp.slots_service.controller import org.springframework.beans.factory.annotation.Autowired -import org.springframework.format.annotation.DateTimeFormat import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -32,8 +31,8 @@ class BookingController { @GetMapping("/search") fun search( - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) timeStart: LocalDateTime?, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) timeStop: LocalDateTime?, + @RequestParam(required = false) timeStart: LocalDateTime?, + @RequestParam(required = false) timeStop: LocalDateTime?, @RequestParam(required = false) requestId: String?, @RequestParam(required = false) satelliteIds: List? ) = slotService.searchBookedSlots( diff --git a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/entity/SatelliteIcEntity.kt b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/entity/SatelliteIcEntity.kt index 0eab57e..548c837 100644 --- a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/entity/SatelliteIcEntity.kt +++ b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/entity/SatelliteIcEntity.kt @@ -4,6 +4,7 @@ import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.Id import jakarta.persistence.Table +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @Entity @@ -13,7 +14,7 @@ class SatelliteIcEntity( @Column(name = "satellite_id", nullable = false) val satelliteId: Long = 0, @Column(name = "orb_time", nullable = false) - val orbTime: LocalDateTime = LocalDateTime.now(), + val orbTime: LocalDateTime = UtcDateTimes.now(), @Column(name = "orb_revolution", nullable = false) val orbRevolution: Long = 0, @Column(nullable = false) diff --git a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/entity/SlotEntity.kt b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/entity/SlotEntity.kt index 4c4dd8c..612f2ad 100644 --- a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/entity/SlotEntity.kt +++ b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/entity/SlotEntity.kt @@ -9,6 +9,7 @@ import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotHolderDTO import space.nstart.pcp.pcp_types_lib.utils.wkt.WKTParser import space.nstart.pcp.slots_service.model.Mar +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @@ -27,9 +28,9 @@ class SlotEntity( @Column(nullable = false) val coveringType : Int = 0, @Column(nullable = false) - var tn : LocalDateTime = LocalDateTime.now(), + var tn : LocalDateTime = UtcDateTimes.now(), @Column(nullable = false) - var tk : LocalDateTime = LocalDateTime.now(), + var tk : LocalDateTime = UtcDateTimes.now(), val roll : Double = 0.0, @Column(name = "contour_wkt", columnDefinition = "TEXT", nullable = false) val contour : String = "", diff --git a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/model/Mar.kt b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/model/Mar.kt index 7ea770f..0e2eed0 100644 --- a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/model/Mar.kt +++ b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/model/Mar.kt @@ -2,12 +2,13 @@ package space.nstart.pcp.slots_service.model import org.locationtech.jts.geom.Geometry import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime class Mar ( val sat : Long = 0, - val tn : LocalDateTime = LocalDateTime.now(), - val tk : LocalDateTime = LocalDateTime.now(), + val tn : LocalDateTime = UtcDateTimes.now(), + val tk : LocalDateTime = UtcDateTimes.now(), val roll : Double = 0.0, val contour : String = "", var poly : Geometry? = null, diff --git a/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/configuration/Jackson2UtcConfiguration.kt b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/configuration/Jackson2UtcConfiguration.kt new file mode 100644 index 0000000..77fcf47 --- /dev/null +++ b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/configuration/Jackson2UtcConfiguration.kt @@ -0,0 +1,41 @@ +package space.nstart.pcp.tle_monitoring_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("tle-monitoring-service-jackson2-utc-local-date-time") + .addSerializer(LocalDateTime::class.java, Jackson2UtcLocalDateTimeSerializer()) + .addDeserializer(LocalDateTime::class.java, Jackson2UtcLocalDateTimeDeserializer()) + +private class Jackson2UtcLocalDateTimeSerializer : JsonSerializer() { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class Jackson2UtcLocalDateTimeDeserializer : JsonDeserializer() { + 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) + } +} diff --git a/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/configuration/UtcTimeConfiguration.kt b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/configuration/UtcTimeConfiguration.kt new file mode 100644 index 0000000..0d3060c --- /dev/null +++ b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/configuration/UtcTimeConfiguration.kt @@ -0,0 +1,55 @@ +package space.nstart.pcp.tle_monitoring_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 tleMonitoringUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder -> + builder.addModule( + SimpleModule("tle-monitoring-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 { + override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source) +} + +private class UtcLocalDateTimeSerializer : StdSerializer(LocalDateTime::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) { + gen.writeString(UtcDateTimes.format(value)) + } +} + +private class UtcLocalDateTimeDeserializer : StdDeserializer(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) + } +} diff --git a/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/controller/TLEController.kt b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/controller/TLEController.kt index 015a175..68dff55 100644 --- a/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/controller/TLEController.kt +++ b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/controller/TLEController.kt @@ -27,6 +27,11 @@ class TLEController { @Autowired private lateinit var celestrakService: CelesTrakService + + @PostMapping("/satellite/{satelliteId}") + fun sendTLE(@PathVariable satelliteId: Long, @RequestBody tle : TLEDTO) = + celestrakService.sendTLE(satelliteId, tle) + @GetMapping() fun all(): List { logger.info("TLE API all records request started") diff --git a/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/entity/TLEEntity.kt b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/entity/TLEEntity.kt index 8541ad5..974cd39 100644 --- a/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/entity/TLEEntity.kt +++ b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/entity/TLEEntity.kt @@ -1,6 +1,7 @@ package space.nstart.pcp.tle_monitoring_service.entity import jakarta.persistence.* +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import java.time.LocalDateTime @@ -22,5 +23,5 @@ class TLEEntity ( val tle : String = "", var timeTle : LocalDateTime? = null, @Column(name = "time_rec", nullable = false) - val timeRec : LocalDateTime = LocalDateTime.now() + val timeRec : LocalDateTime = UtcDateTimes.now() ) diff --git a/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/service/CelesTrakService.kt b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/service/CelesTrakService.kt index 67997d5..56198a2 100644 --- a/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/service/CelesTrakService.kt +++ b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/service/CelesTrakService.kt @@ -17,11 +17,13 @@ import reactor.core.publisher.Flux import reactor.core.publisher.Mono import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO import space.nstart.pcp.pcp_types_lib.message.KafkaMessage +import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent import space.nstart.pcp.tle_monitoring_service.configuration.CustomErrorException import space.nstart.pcp.tle_monitoring_service.entity.TLEEntity import space.nstart.pcp.tle_monitoring_service.message.KafkaProducer import space.nstart.pcp.tle_monitoring_service.repository.TLERepository +import java.time.Clock import java.time.Duration import java.time.LocalDateTime import java.util.UUID @@ -34,6 +36,7 @@ import kotlin.text.split class CelesTrakService( private val webClient: WebClient, private val pollingSettingsService: TlePollingSettingsService, + private val clock: Clock = Clock.systemUTC(), ) { private val logger = LoggerFactory.getLogger(this::class.java) private val objectMapper = jacksonObjectMapper() @@ -130,6 +133,10 @@ class CelesTrakService( } + fun sendTLE(satelliteId : Long, tle : TLEDTO){ + mkTle(tle, satelliteId, true, true) + } + /** * Получение TLE в сыром текстовом формате @@ -303,7 +310,7 @@ class CelesTrakService( .sortedBy { tle -> tle.timeTle } var first = true var vit = 0 - var tVuz = LocalDateTime.now() + var tVuz = UtcDateTimes.now(clock) var lVuz = 0.0 for(t in tles){ var s = t.tle.split("\r\n") diff --git a/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/service/TlePollingSettingsService.kt b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/service/TlePollingSettingsService.kt index 20858f6..3991444 100644 --- a/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/service/TlePollingSettingsService.kt +++ b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/service/TlePollingSettingsService.kt @@ -4,6 +4,7 @@ import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import space.nstart.pcp.tle_monitoring_service.configuration.CustomValidationException import space.nstart.pcp.tle_monitoring_service.configuration.TlePollingProperties +import java.time.Clock import java.time.Duration import java.time.Instant import java.util.concurrent.atomic.AtomicBoolean @@ -21,7 +22,10 @@ data class UpdateTlePollingSettingsRequest( ) @Service -class TlePollingSettingsService(properties: TlePollingProperties) { +class TlePollingSettingsService( + properties: TlePollingProperties, + private val clock: Clock = Clock.systemUTC(), +) { private val logger = LoggerFactory.getLogger(this::class.java) private val enabled = AtomicBoolean(properties.enabled) private val timeoutSeconds = AtomicLong(validateTimeout(properties.timeoutSeconds)) @@ -55,7 +59,7 @@ class TlePollingSettingsService(properties: TlePollingProperties) { return settings } - fun shouldStartPolling(now: Instant = Instant.now()): Boolean { + fun shouldStartPolling(now: Instant = clock.instant()): Boolean { if (!enabled.get()) { return false } @@ -64,7 +68,7 @@ class TlePollingSettingsService(properties: TlePollingProperties) { return Duration.between(lastStartedAt, now).seconds >= timeoutSeconds.get() } - fun markPollingStarted(now: Instant = Instant.now()) { + fun markPollingStarted(now: Instant = clock.instant()) { lastPollStartedAt.set(now) }