Фронт (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).
8.6 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 | LocalDateTime (planId/plan times) |
да | задокументирован «=UTC» (PlanResponse); баги конвертаций исправлены |
| pcp-ui-service | LocalDateTime (агрегатор/CZML, summaries) |
да | задокументировать «=UTC»; кандидат на Z |
| 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) — naiveLocalDateTimeбез зоны.RequestResponseDtoContractTest(pcp-request-service) —OffsetDateTimeсZ(эталон).TguPlanningDtoContractTest(pcp-ui-service) — оба формата: naiveLocalDateTime+InstantсZ.
Прочие сервисы с naive-временем (ballistics, complex-mission, dynamic-plan, slots, coverage-scheme,
mission-planing) используют тот же дефолтный Jackson-конфиг → тот же naive-формат; при желании
пер-сервисный страж копируется по образцу PlanResponseContractTest.
5. Правило для нового кода
- Frontend: только
utils/utcTime(стражutcTime.guard.test.tsне даст просочиться нарушителю). - Backend: UTC-
now; на новых API предпочтителен явныйZ(OffsetDateTime/Instant). - Меняешь формат на проводе — обнови потребителей и добавь сериализационный тест (контракт не должен «уехать» молча).