pcp: единообразие времени (UTC) по группе сервисов
Фронт (pcp-tgu-ops-ui): - тест-страж utcTime.guard.test.ts: запрещает Date.parse / new Date(string) / toLocale* без timeZone вне utils/utcTime; тестовые фикстуры переведены на parseUtc. Бэкенд: - UTC-now: LocalDateTime.now() -> LocalDateTime.now(ZoneOffset.UTC) по всем сервисам (entity-дефолты, воркеры, use case'ы) и в тестовых фикстурах. - Конвертации к UTC (баги на не-UTC JVM, каждая с регресс-тестом в не-UTC поясе): - tgu: VisibilityPayloadParser, SyncSpacecraftFromNsiUseCase (Instant -> UTC LocalDateTime); - mission-planing: MissionPlaningService (OffsetDateTime -> UTC через toUtcLocalDateTime); - dynamic-plan: RequestServiceClient (OffsetDateTime -> UTC); - slots: SlotRepositoryImpl (getObject(LocalDateTime) вместо getTimestamp().toLocalDateTime()). - Контрактные сериализационные тесты формата на проводе (naive без зоны / Z): PlanResponse (tgu), RequestResponseDto (request, эталон), TguPlanning DTO (ui). - KDoc-инвариант «=UTC» на UI-facing DTO (tgu PlanResponse, ui-service DTO вкладки ТГУ). Документация: docs/standards/DATETIME_UTC.md (соглашение, таблица сервисов, под-задачи на миграцию к Z), docs/standards/CHECKLIST.md (пункт про UTC на API).
This commit is contained in:
+3
-2
@@ -10,6 +10,7 @@ import jakarta.persistence.JoinColumn
|
||||
import jakarta.persistence.ManyToOne
|
||||
import jakarta.persistence.Table
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
|
||||
@Table(name = "dynamic_plan_route")
|
||||
@@ -31,10 +32,10 @@ class DynamicPlanRouteEntity(
|
||||
var satelliteId: Long = 0,
|
||||
|
||||
@Column(name = "start_time", nullable = false)
|
||||
var startTime: LocalDateTime = LocalDateTime.now(),
|
||||
var startTime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
|
||||
|
||||
@Column(name = "end_time", nullable = false)
|
||||
var endTime: LocalDateTime = LocalDateTime.now(),
|
||||
var endTime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
|
||||
|
||||
@Column(name = "duration", nullable = false)
|
||||
var duration: Double = 0.0,
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ import space.nstart.pcp.complan.types.DynamicPlanCalculationMode
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRevolutionMode
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
|
||||
@Table(name = "dynamic_plan_run")
|
||||
@@ -47,7 +48,7 @@ class DynamicPlanRunEntity(
|
||||
var filterCoveredRoutes: Boolean = false,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
var createdAt: LocalDateTime = LocalDateTime.now(),
|
||||
var createdAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
|
||||
|
||||
@Column(name = "started_at")
|
||||
var startedAt: LocalDateTime? = null,
|
||||
|
||||
+6
-5
@@ -25,6 +25,7 @@ import space.nstart.pcp.complan.types.DynamicPlanRunStatus
|
||||
import space.nstart.pcp.complan.types.IntervalItemDTO
|
||||
import space.nstart.pcp.complan.types.SurveyContour
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ExecutorService
|
||||
import kotlin.math.roundToLong
|
||||
@@ -59,7 +60,7 @@ class DynamicPlanRunService(
|
||||
calculationMode = request.calculationMode,
|
||||
revolutionMode = request.revolutionMode,
|
||||
filterCoveredRoutes = request.filterCoveredRoutes,
|
||||
createdAt = LocalDateTime.now()
|
||||
createdAt = LocalDateTime.now(ZoneOffset.UTC)
|
||||
)
|
||||
)
|
||||
} ?: 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 = LocalDateTime.now(ZoneOffset.UTC)
|
||||
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 = LocalDateTime.now(ZoneOffset.UTC)
|
||||
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 = LocalDateTime.now(ZoneOffset.UTC)
|
||||
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 = LocalDateTime.now(ZoneOffset.UTC)
|
||||
run.errorMessage = error.message ?: error::class.java.simpleName
|
||||
runRepository.save(run)
|
||||
}
|
||||
|
||||
+5
-2
@@ -8,6 +8,7 @@ import org.springframework.web.client.RestClient
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import space.nstart.pcp.complan.types.RequestItemDTO
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
|
||||
/** Получение заявок из pcp-request-service /v1 API. */
|
||||
@@ -43,8 +44,10 @@ class RequestServiceClient {
|
||||
id = id,
|
||||
name = name,
|
||||
geometry = geometry,
|
||||
intervalBegin = beginDateTime.toLocalDateTime(),
|
||||
intervalEnd = endDateTime.toLocalDateTime(),
|
||||
// Нормализуем к UTC: pcp-request-service шлёт `Z`, но голый toLocalDateTime() уехал бы
|
||||
// при любом ненулевом офсете. Все naive LocalDateTime в системе — UTC (docs/standards/DATETIME_UTC.md).
|
||||
intervalBegin = beginDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(),
|
||||
intervalEnd = endDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(),
|
||||
resolution = resolution(),
|
||||
importance = importance,
|
||||
)
|
||||
|
||||
+3
-2
@@ -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 java.time.ZoneOffset
|
||||
|
||||
/**
|
||||
* Класс космического аппарата**
|
||||
@@ -17,9 +18,9 @@ class SpaceCraft (
|
||||
val flightLine : MutableList<FlightLineDTO> = mutableListOf()
|
||||
|
||||
/**Время начала планирования аппарата*/
|
||||
var timeStart: LocalDateTime = LocalDateTime.now()
|
||||
var timeStart: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
|
||||
/**Время конца планирования аппарата*/
|
||||
var timeStop : LocalDateTime = LocalDateTime.now()
|
||||
var timeStop : LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
|
||||
/**
|
||||
* Создание линий полета восходящего и нисходящего витка
|
||||
* @param geomFactory фабрика геометрии
|
||||
|
||||
+22
-2
@@ -62,9 +62,29 @@ class RequestServiceClientTest {
|
||||
assertFalse(requestedUris.any { uri -> uri.path.startsWith("/api/v1/requests") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getRequest normalizes non-UTC offsets to UTC LocalDateTime`() {
|
||||
val requestId = UUID.fromString("00000000-0000-0000-0000-000000000002")
|
||||
// Источник прислал то же мгновение, но со смещением +03:00. Голый toLocalDateTime() оставил бы
|
||||
// локальную стенку (06:04:05); нормализация к UTC обязана дать 03:04:05.
|
||||
server = serverWithRequestResponse(
|
||||
mutableListOf(),
|
||||
requestId,
|
||||
beginDateTime = "2026-01-02T06:04:05+03:00",
|
||||
endDateTime = "2026-01-03T07:05:06+03:00",
|
||||
)
|
||||
|
||||
val request = requestServiceClient().getRequest(requestId)!!
|
||||
|
||||
assertEquals(LocalDateTime.of(2026, 1, 2, 3, 4, 5), request.intervalBegin)
|
||||
assertEquals(LocalDateTime.of(2026, 1, 3, 4, 5, 6), request.intervalEnd)
|
||||
}
|
||||
|
||||
private fun serverWithRequestResponse(
|
||||
requestedUris: MutableList<URI>,
|
||||
requestId: UUID,
|
||||
beginDateTime: String = "2026-01-02T03:04:05Z",
|
||||
endDateTime: String = "2026-01-03T04:05:06Z",
|
||||
): HttpServer =
|
||||
HttpServer.create(InetSocketAddress(0), 0).apply {
|
||||
createContext("/v1/requests") { exchange ->
|
||||
@@ -77,8 +97,8 @@ class RequestServiceClientTest {
|
||||
"surveyType": "OPTICS",
|
||||
"geometry": "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
"importance": 7.5,
|
||||
"beginDateTime": "2026-01-02T03:04:05Z",
|
||||
"endDateTime": "2026-01-03T04:05:06Z",
|
||||
"beginDateTime": "$beginDateTime",
|
||||
"endDateTime": "$endDateTime",
|
||||
"kpp": [1],
|
||||
"highPriorityTransmit": false,
|
||||
"optics": {
|
||||
|
||||
Reference in New Issue
Block a user