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).
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
- существующие публичные контракты не сломаны без явного запроса;
|
||||
- добавлены или обновлены релевантные тесты;
|
||||
- не внесено несвязанных правок;
|
||||
- время на API — UTC; backend использует UTC-`now` (`Instant.now()` / `LocalDateTime.now(ZoneOffset.UTC)`), фронт парсит/форматирует через `utils/utcTime` (см. `docs/standards/DATETIME_UTC.md`);
|
||||
- validation выполнен по правилам `AGENTS.md`, либо пропуски явно объяснены.
|
||||
|
||||
### Дополнительно для BPM / Kafka / Zeebe / external integration изменений
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# 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`).
|
||||
- Меняешь формат на проводе — обнови потребителей и добавь сериализационный тест (контракт не должен «уехать» молча).
|
||||
Reference in New Issue
Block a user