PlanResponse (pcp-tgu-service) и зависящий от него TguPlanResponse (pcp-ui-service) переведены с naive LocalDateTime на OffsetDateTime в UTC → на проводе явная зона Z. Домен/БД (PlannedPlanEntity/CalculatedPlan) остаются naive LocalDateTime (= UTC); конвертация только на границе API (atOffset(ZoneOffset.UTC)). Потребитель один (ui-service.TguPlanningService), обновлён синхронно; фронт parseUtc совместим с Z. - контрактные тесты обновлены на Z (PlanResponseContractTest, TguPlanningDtoContractTest); - тестовые фикстуры/ассерты приведены к OffsetDateTime/Z (PlanQueryServiceTest, CatalogControllerTest, TguPlanningServiceTest); - docs/standards/DATETIME_UTC.md: отражена миграция и обновлена таблица сервисов.
9.7 KiB
DATETIME_UTC.md — единообразие времени (UTC) в группе сервисов pcp
Назначение
Этот документ фиксирует соглашение по работе со временем во всей группе сервисов pcp
и состояние сервисов на момент аудита. На него ссылаются KDoc у DTO и комментарии в коде.
Цель — убрать неоднозначность парсинга/форматирования и неоднородность формата на проводе,
которая уже приводила к сдвигу времени на офсет пояса (для МСК — +3 ч).
1. Соглашение (canonical)
- Всё время — UTC. На любом API времена представляют момент в UTC. В БД — тоже UTC.
- Формат на проводе — ISO-8601 с явной зоной
Z(предпочтительно). Где сервис исторически отдаёт naiveLocalDateTime(ISO без зоны, напр."2026-05-29T01:00:00"), это допустимо только при железном правиле «naive = UTC»; такой эндпоинт — кандидат на миграцию кZ. - Backend:
- «сейчас» — только UTC:
Instant.now()илиLocalDateTime.now(ZoneOffset.UTC); неLocalDateTime.now(); - конвертация
Instant/OffsetDateTime→LocalDateTimeдля naive-полей — только через UTC (LocalDateTime.ofInstant(instant, ZoneOffset.UTC),withOffsetSameInstant(ZoneOffset.UTC)), никогда черезZoneId.systemDefault()/atZone(systemDefault()); - Jackson во всех сервисах настроен с
JavaTimeModule+disable(WRITE_DATES_AS_TIMESTAMPS), поэтомуLocalDateTimeуходит как ISO без зоны, аOffsetDateTime/Instant— сZ/офсетом.
- «сейчас» — только UTC:
- Frontend (
pcp-tgu-ops-ui):- парсинг строк бэка — только через
src/utils/utcTime.ts(parseUtc); никаких «голых»new Date(string)/Date.parse(string); - отображение — только UTC (
formatUtcDateTime/Date/TimeлибоtoLocale*сtimeZone: "UTC"); <input type="datetime-local">— значение черезtoUtcInputValue, чтение черезparseUtc, отправка на offset-бэк — черезtoUtcIso.- Инвариант закреплён тестом-стражем
src/utils/utcTime.guard.test.ts(падает на запрещённых паттернах).
- парсинг строк бэка — только через
- Никакой неявной локализации под пояс машины ни на фронте, ни в логах/ответах бэка.
2. Эталоны переиспользования
- Frontend:
services/pcp-tgu-ops-ui/src/utils/utcTime.ts— единая точка парсинга/форматирования. - Backend:
pcp-request-serviceотдаётOffsetDateTime(строки сZ) и конвертирует вход черезOffsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime()— корректный образец.
3. Состояние сервисов (срез аудита)
Формат на проводе определяется Jackson-сериализацией: LocalDateTime → ISO без зоны (naive, = UTC),
OffsetDateTime/Instant → ISO с Z. «Потребители» — кто читает время этого сервиса.
| Сервис | Преобладает на границе API | Naive? | Вердикт |
|---|---|---|---|
| pcp-request-service | OffsetDateTime (Z) |
нет | эталон: явная зона на проводе |
| pcp-tgu-service | REST PlanResponse — OffsetDateTime (Z); Kafka/прочие DTO — LocalDateTime |
частично | PlanResponse мигрирован на Z; баги конвертаций исправлены; Kafka-DTO остаются naive (=UTC) |
| pcp-ui-service | LocalDateTime (агрегатор/CZML, summaries); план ТГУ — OffsetDateTime (Z) |
частично | TguPlanResponse мигрирован на Z вслед за tgu; прочие naive DTO — задокументировать |
| pcp-complex-mission-service | LocalDateTime (+ часть Instant) |
смешано | проверить смешение; задокументировать |
| pcp-dynamic-plan-service | LocalDateTime (+ offset на входе клиента) |
да | задокументировать «=UTC» |
| pcp-ballistics-service | LocalDateTime |
да | задокументировать «=UTC» |
| pcp-mission-planing-service | LocalDateTime (вход — OffsetDateTime) |
да | задокументировать «=UTC» |
| slots-service | LocalDateTime |
да | задокументировать «=UTC»; проверить rs.getTimestamp().toLocalDateTime() |
| pcp-coverage-scheme-service | LocalDateTime |
да | задокументировать «=UTC» |
| pcp-route-processing-service | нет времени на границе API | — | — |
| pcp-satellite-catalog-service | нет времени на границе API | — | — |
| pcp-stations-service | нет времени на границе API | — | — |
| tle-monitoring-service | нет времени на границе API | — | — |
4. Известные расхождения и под-задачи (миграция к Z — не «большим взрывом»)
Систематический класс: LocalDateTime.now() без UTC-зоны (~80 мест: entity-дефолты createdAt/updatedAt,
воркеры, use case'ы). На не-UTC JVM пишет локальное «сейчас» в UTC-поля. Низкий риск для audit-колонок,
но формально неверно. Рекомендованные под-задачи по сервисам:
- pcp-tgu-service — заменить оставшиеся
LocalDateTime.now()(воркеры выдачи/решений,PlanQueryServicecutoff, entity-дефолты) наLocalDateTime.now(ZoneOffset.UTC); тест на cutoff истории. - slots-service — проверить
SlotRepositoryImpl.rs.getTimestamp("tn").toLocalDateTime()(зависит от зоны драйвера); зафиксировать чтение в UTC. - pcp-mission-planing-service / pcp-dynamic-plan-service —
OffsetDateTime.toLocalDateTime()теряет офсет; убедиться, что вход всегда UTC, иначе конвертировать черезwithOffsetSameInstant(UTC). - Все сервисы с naive
LocalDateTimeна границе — добавить сериализационный тест, фиксирующий формат на проводе (естьZ/ нет зоны), затем по согласованию мигрировать DTO кOffsetDateTime/Instant(UTC). ФронтовыйparseUtcуже совместим с обоими форматами, поэтому миграция безопасна при пер-сервисном раскате.
Контрактные тесты формата (зафиксировано)
Оба формата на проводе закреплены сериализационными тестами (mapper зеркалит конфиг Spring Boot —
JavaTimeModule + disable(WRITE_DATES_AS_TIMESTAMPS)):
PlanResponseContractTest(pcp-tgu-service) —OffsetDateTimeсZ(после миграции, см. ниже).RequestResponseDtoContractTest(pcp-request-service) —OffsetDateTimeсZ(эталон).TguPlanningDtoContractTest(pcp-ui-service) — план ТГУOffsetDateTimeсZ+InstantсZ.
Прочие сервисы с naive-временем (ballistics, complex-mission, dynamic-plan, slots, coverage-scheme,
mission-planing) используют тот же дефолтный Jackson-конфиг → тот же naive-формат; при желании
пер-сервисный страж копируется по образцу RequestResponseDtoContractTest.
Миграция к Z — выполнено
- План ТГУ (tgu → ui-service → фронт).
PlanResponse(pcp-tgu-service) и зависящий от негоTguPlanResponse(pcp-ui-service) переведены с naiveLocalDateTimeнаOffsetDateTimeв UTC → на проводеZ. Домен/БД (PlannedPlanEntity/CalculatedPlan) остались naiveLocalDateTime(=UTC); конвертация только на границе (atOffset(ZoneOffset.UTC)). Потребитель один (ui-service), обновлён синхронно; фронтparseUtcсовместим. Контракт закреплён тестами выше.
Следующие кандидаты на миграцию (по согласованию, по сервису): остальные naive-DTO ui-service, ballistics, complex-mission, dynamic-plan, slots, coverage-scheme.
5. Правило для нового кода
- Frontend: только
utils/utcTime(стражutcTime.guard.test.tsне даст просочиться нарушителю). - Backend: UTC-
now; на новых API предпочтителен явныйZ(OffsetDateTime/Instant). - Меняешь формат на проводе — обнови потребителей и добавь сериализационный тест (контракт не должен «уехать» молча).