pcp: откат UTC-слоя — сквозной naive LocalDateTime без зоны на проводе
Возврат к модели «настенных часов»: одна ISO-строка без зоны
("2026-05-29T01:00:00") от БД до экрана, никто не привязывает её к
часовому поясу, сдвигаться нечему.
Бэкенд — снять зону с границы API:
- DTO OffsetDateTime/Instant -> LocalDateTime и убрана конвертация
atOffset(ZoneOffset.UTC)/toUtcOffset() в фабриках и мапперах:
tgu PlanResponse/NewChainDTOs/TguPlanningDTO, request RequestApiDtos,
complex ComplexPlanRun*DTO, dynamic ObservationParametersDebugDTO.
- Удалён толерантный Jackson-слой: PcpTimeModule(+autoconfig, ServiceLoader,
imports), PcpTimeModuleV3, LocalDateTimeTimeZoneSerializer; снята их
регистрация в Camunda/Rest/dynamic JacksonConfig. Штатный JSR-310
(JavaTimeModule + WRITE_DATES_AS_TIMESTAMPS=false) оставлен.
- Kafka-конверт KafkaMessage.time сериализуется ISO без офсета.
- mission-planing Mode/SurveyMode/DropMode entity и репозитории/сервис
переведены на LocalDateTime; колонки БД остаются timestamptz (Hibernate
мапит на naive без миграции — прецедент MissionEntity/PlanEntity).
- RequestServiceClient (dynamic) синхронизирован по типу с request-service.
Анти-регрессия: LocalDateTime.now(ZoneOffset.UTC) для серверных штампов
оставлен (выбор настенных часов UTC, не зона на проводе).
Фронт — снять офсет с egress: toUtcIso/фильтры отдают naive без Z
(после удаления толерантного десериализатора штатный парсер падал бы на Z).
Дисплейные хелперы (показ «как пришло», без сдвига) сохранены.
Тесты: контрактные ждут naive ISO; удалены UTC-артефакты (нормализация
офсетов) и тесты прослоек. Удалён docs/standards/DATETIME_UTC.md.
Операционно перед выкатом: сервисы катить согласованно; Kafka-топики с
ранее опубликованным офсетом +00:00 дренировать/проиграть.
This commit is contained in:
@@ -21,8 +21,8 @@
|
||||
- существующие публичные контракты не сломаны без явного запроса;
|
||||
- добавлены или обновлены релевантные тесты;
|
||||
- не внесено несвязанных правок;
|
||||
- время на API — UTC; backend использует UTC-`now` (`Instant.now()` / `LocalDateTime.now(ZoneOffset.UTC)`), фронт парсит/форматирует через `utils/utcTime` (см. `docs/standards/DATETIME_UTC.md`);
|
||||
- чтение времени на бэке толерантно к обоим форматам (naive=UTC и `Z`) — модуль наследуется из `pcp-types-lib` (`PcpTimeModule`) авто-конфигурацией; ручной `ObjectMapper`/не-pcp-types-lib сервис регистрирует модуль явно;
|
||||
- время на API — naive `LocalDateTime` без зоны; backend штампует `now` через `LocalDateTime.now(ZoneOffset.UTC)`, фронт парсит/форматирует через `utils/utcTime` без `new Date("<строка>")`;
|
||||
- egress фронта к бэку — naive ISO (`YYYY-MM-DDTHH:mm:ss`, без Z);
|
||||
- validation выполнен по правилам `AGENTS.md`, либо пропуски явно объяснены.
|
||||
|
||||
### Дополнительно для BPM / Kafka / Zeebe / external integration изменений
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
# 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 (чтение): `libs/pcp-types-lib` `utils/json/PcpTimeModule` — слой толерантности
|
||||
(бэковый аналог `parseUtc`): любой потребитель читает и naive(=UTC), и `Z`/офсет (см. раздел ниже).
|
||||
- Backend (запись): `pcp-request-service` отдаёт `OffsetDateTime` (строки с `Z`) и конвертирует вход
|
||||
через `OffsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime()` — корректный образец.
|
||||
|
||||
## 2bis. Слой толерантности времени (включатель безопасного флипа)
|
||||
|
||||
`PcpTimeModule` (`libs/pcp-types-lib/.../utils/json/PcpTimeModule.kt`) — Jackson-модуль с
|
||||
толерантными **десериализаторами** для `LocalDateTime`, `OffsetDateTime`, `Instant`. Любой
|
||||
потребитель с ним читает время в **обоих** форматах на проводе и нормализует к UTC:
|
||||
|
||||
| Тип поля | naive `…T01:00:00` | с зоной `…T01:00:00Z` / `+03:00` |
|
||||
|---|---|---|
|
||||
| `LocalDateTime` | как есть (= UTC) | приводится к UTC, зона отбрасывается |
|
||||
| `OffsetDateTime` | `atOffset(UTC)` | `withOffsetSameInstant(UTC)` |
|
||||
| `Instant` | трактуется как момент UTC | штатно |
|
||||
|
||||
Модуль трогает **только чтение** — формат записи определяется типом поля и не меняется, поэтому
|
||||
подключение слоя само по себе не сдвигает ни один контракт.
|
||||
|
||||
**Зачем.** Микросервисы деплоятся независимо. Раньше перевод продюсера `LocalDateTime` → `OffsetDateTime`
|
||||
(на проводе появляется `Z`) ломал ещё не обновлённого потребителя — штатный `LocalDateTimeDeserializer`
|
||||
кидает исключение на офсете. С толерантностью во всём флоте продюсера можно флипать **в любом порядке,
|
||||
без синхронного деплоя**. Это снимает требование «продюсер + все потребители одним коммитом».
|
||||
|
||||
**Подключение (три механизма, чтобы покрыть все мапперы):**
|
||||
- Spring Boot auto-configuration `PcpTimeJacksonAutoConfiguration` (`@Bean Module`) +
|
||||
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` — Boot-managed
|
||||
`ObjectMapper` каждого из 12 сервисов, зависящих от pcp-types-lib (правок в сервисах не требуется).
|
||||
- Jackson SPI `META-INF/services/com.fasterxml.jackson.databind.Module` — ручные мапперы с
|
||||
`findAndRegisterModules()` (напр. `pcp-request-service`).
|
||||
- Точечная регистрация в мапперах, что обходят оба: `pcp-dynamic-plan-service/JacksonConfig`.
|
||||
|
||||
**Особый случай — pcp-tgu-service.** Сервис мигрирован с WebFlux на **servlet/MVC** (см. ниже
|
||||
«Миграция tgu на MVC») и теперь зависит от pcp-types-lib, как остальной флот. Но из-за Boot 4 на
|
||||
HTTP-границе всё равно работает **Jackson 3**, поэтому время покрывается двумя путями:
|
||||
- Jackson-2 модуль **из либы** (`pcp_types_lib.utils.json.PcpTimeModule`) — явно зарегистрирован в
|
||||
ручном маппере `CamundaJacksonConfig` (Camunda/внутренний парсинг); своего зеркала больше нет.
|
||||
- `config/PcpTimeModuleV3.kt` (**Jackson 3**, `tools.jackson.*`) — REST HTTP message converters (MVC),
|
||||
которые Boot 4 авто-конфигурирует на Jackson 3 `JsonMapper`. Регистрируется бином
|
||||
`tools.jackson.databind.JacksonModule` (`RestJacksonConfig`); `JacksonAutoConfiguration` собирает
|
||||
все такие бины в общий `JsonMapper` (`builder.addModules(...)`). Лежит локально, т.к. либа даёт
|
||||
только Jackson-2 модуль; поведение обязано совпадать с библиотечным, правки синхронизировать.
|
||||
|
||||
Контракт слоя закреплён `PcpTimeModuleTest` (pcp-types-lib): оба формата → один и тот же UTC-момент,
|
||||
плюс проверка авто-подхвата через SPI.
|
||||
|
||||
## 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` совместим. Контракт закреплён тестами выше.
|
||||
|
||||
- **Слой толерантности (`PcpTimeModule`) — раскатан по флоту.** 12 сервисов на pcp-types-lib
|
||||
получают его авто-конфигурацией/SPI; dynamic-plan — точечно; tgu — модулем из либы (Camunda,
|
||||
Jackson 2) плюс локальное Jackson-3 зеркало для REST-границы MVC. Это включатель: дальнейший флип
|
||||
продюсеров на `Z` больше не требует синхронного деплоя.
|
||||
|
||||
После толерантности продюсеров можно флипать **в любом порядке**. Кандидаты: остальные naive-DTO
|
||||
ui-service, ballistics, complex-mission, dynamic-plan, slots, coverage-scheme.
|
||||
|
||||
### Исправленные баги формата
|
||||
|
||||
- **`KafkaMessage.time` (общий Kafka-конверт, pcp-types-lib) — исправлено.** Было `time = LocalDateTime.now()`
|
||||
(локальное!) + сериализация `LocalDateTimeTimeZoneSerializer` через `ZoneId.systemDefault()`: конверт
|
||||
нёс МСК-стенные часы с `+03:00`, а потребитель (формат `…SSS[xxx]` в `LocalDateTime`) отбрасывал зону
|
||||
→ сдвиг на пояс. Стало `now(ZoneOffset.UTC)` + сериализация `atOffset(ZoneOffset.UTC)` → на проводе
|
||||
`…+00:00`. **Инстант не изменился** (тот же момент), но стенные часы теперь совпадают с UTC, поэтому
|
||||
потребитель, дропающий зону, получает корректное время. Закреплено `LocalDateTimeTimeZoneSerializerTest`.
|
||||
|
||||
- **pcp-tgu-service REST-граница на Jackson 3** — теперь толерантна (см. 2bis). Добавлен Jackson-3
|
||||
вариант модуля `config/PcpTimeModuleV3.kt` и его регистрация бином `JacksonModule`
|
||||
(`config/RestJacksonConfig.kt`); входящий REST-JSON читается в обоих форматах (naive=UTC и `Z`).
|
||||
Закреплено `PcpTimeModuleV3Test`.
|
||||
|
||||
### Миграция tgu на MVC
|
||||
|
||||
- **pcp-tgu-service: WebFlux → servlet/MVC.** Сервис не имел реального реактивного кода — JPA
|
||||
блокирующая, контроллеры оборачивали блокирующие вызовы в `Mono.fromCallable{}.subscribeOn(
|
||||
boundedElastic())`, WebClient-вызовы `.block()`-ались сразу. Это был единственный WebFlux-only
|
||||
сервис флота. Переведён на MVC: подключён pcp-types-lib (тянет `starter-web` → тип приложения
|
||||
servlet), `springdoc` → `webmvc-ui`, контроллеры возвращают значения напрямую (без `Mono`).
|
||||
`starter-webflux` оставлен только ради WebClient. Побочный эффект для времени: убрано локальное
|
||||
Jackson-2 зеркало (`config/PcpTimeModule.kt`) в пользу модуля из либы; Jackson-3 зеркало остаётся
|
||||
(Boot 4 держит Jackson 3 на HTTP-границе и для MVC).
|
||||
|
||||
### Остаток (под-задачи)
|
||||
|
||||
- Обе Jackson-среды tgu толерантны; отдельных остатков по чтению времени нет. При миграции
|
||||
оставшихся продьюсеров на `Z` (раздел 4) добавлять сериализационные тесты контракта.
|
||||
|
||||
## 5. Правило для нового кода
|
||||
|
||||
- Frontend: только `utils/utcTime` (страж `utcTime.guard.test.ts` не даст просочиться нарушителю).
|
||||
- Backend: UTC-`now`; на новых API предпочтителен явный `Z` (`OffsetDateTime`/`Instant`).
|
||||
- Меняешь формат на проводе — обнови потребителей и добавь сериализационный тест (контракт не должен «уехать» молча).
|
||||
Reference in New Issue
Block a user