Files
dc-observatio/docs/standards/DATETIME_UTC.md
T
Дмитрий Соловьев c2e2b2922f pcp: миграция времени плана ТГУ на явный Z (OffsetDateTime)
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: отражена миграция и обновлена таблица сервисов.
2026-06-02 22:02:13 +03:00

9.7 KiB
Raw Blame History

DATETIME_UTC.md — единообразие времени (UTC) в группе сервисов pcp

Назначение

Этот документ фиксирует соглашение по работе со временем во всей группе сервисов pcp и состояние сервисов на момент аудита. На него ссылаются KDoc у DTO и комментарии в коде. Цель — убрать неоднозначность парсинга/форматирования и неоднородность формата на проводе, которая уже приводила к сдвигу времени на офсет пояса (для МСК — +3 ч).

1. Соглашение (canonical)

  1. Всё время — UTC. На любом API времена представляют момент в UTC. В БД — тоже UTC.
  2. Формат на проводе — ISO-8601 с явной зоной Z (предпочтительно). Где сервис исторически отдаёт naive LocalDateTime (ISO без зоны, напр. "2026-05-29T01:00:00"), это допустимо только при железном правиле «naive = UTC»; такой эндпоинт — кандидат на миграцию к Z.
  3. Backend:
    • «сейчас» — только UTC: Instant.now() или LocalDateTime.now(ZoneOffset.UTC); не LocalDateTime.now();
    • конвертация Instant/OffsetDateTimeLocalDateTime для 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/офсетом.
  4. 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 (падает на запрещённых паттернах).
  5. Никакой неявной локализации под пояс машины ни на фронте, ни в логах/ответах бэка.

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 PlanResponseOffsetDateTime (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() (воркеры выдачи/решений, PlanQueryService cutoff, entity-дефолты) на LocalDateTime.now(ZoneOffset.UTC); тест на cutoff истории.
  • slots-service — проверить SlotRepositoryImpl.rs.getTimestamp("tn").toLocalDateTime() (зависит от зоны драйвера); зафиксировать чтение в UTC.
  • pcp-mission-planing-service / pcp-dynamic-plan-serviceOffsetDateTime.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) переведены с naive LocalDateTime на OffsetDateTime в UTC → на проводе Z. Домен/БД (PlannedPlanEntity/CalculatedPlan) остались naive LocalDateTime (=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).
  • Меняешь формат на проводе — обнови потребителей и добавь сериализационный тест (контракт не должен «уехать» молча).