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

104 lines
9.7 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 | 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()` (воркеры выдачи/решений, `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) — `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`).
- Меняешь формат на проводе — обнови потребителей и добавь сериализационный тест (контракт не должен «уехать» молча).