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:
+2
-1
@@ -12,6 +12,7 @@ import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.support.TransactionCallback
|
||||
import org.springframework.transaction.support.TransactionOperations
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
@Service
|
||||
@@ -69,7 +70,7 @@ class OutboxPublisherService(
|
||||
.get()
|
||||
|
||||
event.status = OutboxEventStatus.PUBLISHED
|
||||
event.publishedAt = LocalDateTime.now()
|
||||
event.publishedAt = LocalDateTime.now(ZoneOffset.UTC)
|
||||
event.errorMessage = null
|
||||
outboxEventRepository.save(event)
|
||||
|
||||
|
||||
+2
-2
@@ -66,7 +66,7 @@ class RequestService(
|
||||
"beginDateTime должен быть раньше endDateTime"
|
||||
}
|
||||
|
||||
val now = LocalDateTime.now()
|
||||
val now = LocalDateTime.now(ZoneOffset.UTC)
|
||||
val normalizedGeometry = geometryService.parsePolygonalGeometry(request.geometry)
|
||||
val normalizedGeometryWkt = geometryService.toWkt(normalizedGeometry)
|
||||
val geometryArea = geometryService.area(normalizedGeometry)
|
||||
@@ -154,7 +154,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 ?: LocalDateTime.now(ZoneOffset.UTC)
|
||||
request.status = RequestStatus.DELETED
|
||||
request.deletedAt = deletedAt
|
||||
request.updatedAt = deletedAt
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.support.TransactionCallback
|
||||
import org.springframework.transaction.support.TransactionOperations
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
@@ -43,7 +44,7 @@ class RouteMatchingService(
|
||||
|
||||
val routeId = route.routeId
|
||||
val routeRollRange = route.rollAngle
|
||||
val matchedAt: LocalDateTime = LocalDateTime.now()
|
||||
val matchedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
|
||||
val routeGeometry = geometryService.parsePolygonalGeometry(route.geometry)
|
||||
|
||||
val candidateRequestIds = requestRepository.findRequestIdsForRoute(
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package org.nstart.dep265.requestservice.dto
|
||||
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Контракт формата времени на проводе для [RequestResponseDto] — эталон группы: `OffsetDateTime`
|
||||
* сериализуется с явной зоной `Z` (см. docs/standards/DATETIME_UTC.md). Тест ловит молчаливый дрейф
|
||||
* (смену типа поля на naive `LocalDateTime` или Jackson-конфига).
|
||||
*
|
||||
* Mapper зеркалит конфиг Spring Boot: JavaTimeModule + disable(WRITE_DATES_AS_TIMESTAMPS).
|
||||
*/
|
||||
class RequestResponseDtoContractTest {
|
||||
|
||||
private val mapper = jacksonObjectMapper().findAndRegisterModules()
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
|
||||
@Test
|
||||
fun `serializes request times as ISO with explicit Z zone`() {
|
||||
val dto = RequestResponseDto(
|
||||
id = UUID.fromString("00000000-0000-0000-0000-000000000001"),
|
||||
name = "request",
|
||||
status = RequestStatusDto.ACTIVE,
|
||||
surveyType = SurveyTypeDto.OPTICS,
|
||||
geometry = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
importance = 1.0,
|
||||
beginDateTime = OffsetDateTime.of(2026, 5, 29, 1, 0, 0, 0, ZoneOffset.UTC),
|
||||
endDateTime = OffsetDateTime.of(2026, 5, 29, 2, 0, 0, 0, ZoneOffset.UTC),
|
||||
highPriorityTransmit = false,
|
||||
coverage = CoverageStateDto(currentPercent = 0.0),
|
||||
createdAt = OffsetDateTime.of(2026, 5, 29, 0, 0, 0, 0, ZoneOffset.UTC),
|
||||
updatedAt = OffsetDateTime.of(2026, 5, 29, 0, 0, 0, 0, ZoneOffset.UTC)
|
||||
)
|
||||
|
||||
val node = mapper.readTree(mapper.writeValueAsString(dto))
|
||||
|
||||
assertEquals("2026-05-29T01:00:00Z", node.get("beginDateTime").asText())
|
||||
assertEquals("2026-05-29T02:00:00Z", node.get("endDateTime").asText())
|
||||
assertTrue(node.get("createdAt").asText().endsWith("Z"), "ожидалась явная зона Z")
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -13,6 +13,7 @@ import org.nstart.dep265.requestservice.repository.EarthCellRepository
|
||||
import org.nstart.dep265.requestservice.repository.RequestCellRepository
|
||||
import org.nstart.dep265.requestservice.repository.RequestRepository
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
@@ -112,14 +113,14 @@ class RequestGridProjectionServiceTest {
|
||||
geometryArea = 1.0,
|
||||
remainingArea = 1.0,
|
||||
importance = importance,
|
||||
beginDateTime = LocalDateTime.now(),
|
||||
endDateTime = LocalDateTime.now().plusDays(1),
|
||||
beginDateTime = LocalDateTime.now(ZoneOffset.UTC),
|
||||
endDateTime = LocalDateTime.now(ZoneOffset.UTC).plusDays(1),
|
||||
highPriorityTransmit = false,
|
||||
surveyType = RequestSurveyType.OPTICS,
|
||||
status = RequestStatus.ACTIVE,
|
||||
matchCount = 0,
|
||||
createdAt = LocalDateTime.now(),
|
||||
updatedAt = LocalDateTime.now(),
|
||||
createdAt = LocalDateTime.now(ZoneOffset.UTC),
|
||||
updatedAt = LocalDateTime.now(ZoneOffset.UTC),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+7
-6
@@ -29,6 +29,7 @@ import org.springframework.transaction.support.SimpleTransactionStatus
|
||||
import org.springframework.transaction.support.TransactionCallback
|
||||
import org.springframework.transaction.support.TransactionOperations
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@@ -47,7 +48,7 @@ class RouteMatchingServiceTest {
|
||||
geometryService = geometryService,
|
||||
)
|
||||
|
||||
val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
|
||||
val intervalBegin = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).withNano(0)
|
||||
val intervalEnd = intervalBegin.plusHours(1)
|
||||
val request = request(
|
||||
beginDateTime = intervalBegin,
|
||||
@@ -96,7 +97,7 @@ class RouteMatchingServiceTest {
|
||||
geometryService = geometryService,
|
||||
)
|
||||
|
||||
val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
|
||||
val intervalBegin = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).withNano(0)
|
||||
val intervalEnd = intervalBegin.plusHours(1)
|
||||
val originalRemainingGeometry = "POLYGON((1 1, 2 1, 2 2, 1 2, 1 1))"
|
||||
val request = request(
|
||||
@@ -146,7 +147,7 @@ class RouteMatchingServiceTest {
|
||||
geometryService = geometryService,
|
||||
)
|
||||
|
||||
val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
|
||||
val intervalBegin = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).withNano(0)
|
||||
val request = request(
|
||||
beginDateTime = intervalBegin,
|
||||
endDateTime = intervalBegin.plusHours(1),
|
||||
@@ -182,7 +183,7 @@ class RouteMatchingServiceTest {
|
||||
geometryService = geometryService,
|
||||
)
|
||||
|
||||
val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
|
||||
val intervalBegin = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).withNano(0)
|
||||
val request = request(
|
||||
beginDateTime = intervalBegin,
|
||||
endDateTime = intervalBegin.plusHours(1),
|
||||
@@ -230,7 +231,7 @@ class RouteMatchingServiceTest {
|
||||
geometryService = geometryService,
|
||||
)
|
||||
|
||||
val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
|
||||
val intervalBegin = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).withNano(0)
|
||||
val request = request(
|
||||
beginDateTime = intervalBegin,
|
||||
endDateTime = intervalBegin.plusHours(1),
|
||||
@@ -268,7 +269,7 @@ class RouteMatchingServiceTest {
|
||||
geometryService = geometryService,
|
||||
)
|
||||
|
||||
val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
|
||||
val intervalBegin = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).withNano(0)
|
||||
val request = request(
|
||||
beginDateTime = intervalBegin,
|
||||
endDateTime = intervalBegin.plusHours(1),
|
||||
|
||||
Reference in New Issue
Block a user