Files
dc-observatio/docs/standards/DATETIME_UTC.md
T
Дмитрий Соловьев 260583b134 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).
2026-06-02 21:51:38 +03:00

93 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`/`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`/офсетом.
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 | `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()` (воркеры выдачи/решений, `PlanQueryService`
cutoff, 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) — naive `LocalDateTime` без зоны.
- `RequestResponseDtoContractTest` (pcp-request-service) — `OffsetDateTime` с `Z` (эталон).
- `TguPlanningDtoContractTest` (pcp-ui-service) — оба формата: naive `LocalDateTime` + `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`).
- Меняешь формат на проводе — обнови потребителей и добавь сериализационный тест (контракт не должен «уехать» молча).