Перейти к основному содержимому

POST /api/admin/v1/projections/rebuild — Принудительный пересчёт карточек по id (admin)

Назначение

Runbook-инструмент устранения расхождений витрины: администратор платформы принудительно пересобирает денормализованные карточки (catalog-property-object-card.md / catalog-project-card.md) по списку id, когда SourceSnapshot уже консистентен, а эффективные поля/Status/агрегаты витрины разошлись (пропущенный пересчёт после редкой гонки консюмеров, ручное вмешательство в БД, откат миграции). Пересчёт не ходит в источники: он идемпотентно переигрывает те же функции проекции, что и консюмеры/пересчёты оверрайдов, поверх уже сохранённых SourceSnapshot + живых оверрайдов + активных правил ранжирования + активных покупок продвижения. Штатный путь синхронизации — консюмеры chessboard/invest-analytics/rewards; этот endpoint — точечная ручная починка по runbook расхождений SourceSnapshot.ChessboardVersion (README каталога, Runtime; chessboard-projection-sync.md).

Metadata

ПолеЗначение
Сервис/контекстcatalog
API areaadmin (ADR-0053; m2m)
Feature groupadmin-projections
Статусdraft
Документdocs/06-services/catalog/api/admin/rebuild-projections.md

Актор и доступ

ПроверкаЗначение
Actor typeExternalSystem (администратор платформы через admin API; m2m client-credentials — ADR-0053)
Auth policy / Permissionspolicy AdminSystemOnly (клиент pin-admin-system); каталожное соответствие — permission catalog.overrides.manage (эксплуатация проекций — под тем же admin-scope; identity/domain/permission.md)
Scope (RBAC)платформа (карточки не tenanted — ADR-0052 п.3)
Auditобязателен: deep audit admin-операции — actor (X-Admin-User-Id), TargetKind, полный список Ids, RebuiltCount, предупреждения, ResetAnalyticsAsOf, результат

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/admin/v1/projections/rebuild
Success status200 (пересчёт выполнен; частично отсутствующие id — в Warnings, не ошибка)
Idempotency headerIdempotency-Key (Guid; обязателен — администратор платформы ретраит по таймауту)
Correlationсквозной trace через traceparent; X-Admin-User-Id (Guid админа) — обязателен

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TargetKindbodystring (enum RebuildTargetKind)Да∈ {Project, PropertyObject} — уровень пересобираемых карточек
IdsbodyGuid[]Да1..MaxBatchSize (default 200); distinct (дубликаты схлопываются + warn); каждый != Guid.Empty; батч ограничен ради одной транзакции и предсказуемого runbook-прогона
ResetAnalyticsAsOfbodyboolНет (default false)true → обнулить SourceSnapshot.AnalyticsAsOf пересобранных карточек, чтобы метрики доехали replay-ем/следующими событиями invest-analytics (см. invest-analytics-enrichment.md, Идемпотентность и replay)
AdminUserIdheader X-Admin-User-IdGuidДа!= Guid.Emptyaudit-stamping
IdempotencyKeyheader Idempotency-KeyGuidДа!= Guid.Empty

Ids — идентификаторы конкретных карточек (= id в chessboard): для TargetKind=Project — id project_cards, для TargetKind=PropertyObject — id property_object_cards.

Модель ответа RebuildProjectionsResult

Именованная container-модель; коллекции ограничены размером входного батча (≤ MaxBatchSize).

ПолеТипОбязательностьИсточникMaskingОписание
TargetKindstring (enum RebuildTargetKind)ДавходУровень пересчёта.
RequestedCountintДавычислениеРазмер Ids после distinct.
RebuiltCountintДавычислениеФактически пересчитанных карточек.
RebuiltIdsIReadOnlyList<Guid>ДапересчётId успешно пересобранных карточек.
WarningsIReadOnlyList<RebuildProjectionWarning>ДавычислениеПропущенные id (не найдено / делистинг). Пустой список — все пересчитаны.

RebuildProjectionWarning

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДавходПропущенный или свёрнутый id.
Reasonstring (enum RebuildWarningReason)ДавычислениеNotFound (карточки нет) / Delisted (карточка Delisted — не пересобирается) / Duplicate (id встречался во входе более одного раза — свёрнут в один пересчёт).

Алгоритм

  1. Контекст и доступ. Актор — администратор платформы (admin API, m2m pin-admin-system, policy AdminSystemOnly); заголовки X-Admin-User-Id, Idempotency-Key. Повтор по ключу admin:projections-rebuild:{IdempotencyKey} → сохранённый RebuildProjectionsResult (200).
  2. Проверка входа. TargetKind ∈ {Project, PropertyObject}; Ids не пуст → иначе CATALOG_REBUILD_EMPTY_BATCH; Ids.Count ≤ MaxBatchSize (проверяется по исходному входу, до distinct) → иначе CATALOG_REBUILD_BATCH_TOO_LARGE; повторяющиеся id схлопываются (RequestedCount = число уникальных id), каждый повтор → Warnings(Duplicate); каждый != Guid.Empty → иначе ValidationError. Существование целей — не жёсткая ошибка: отсутствующие id уходят в Warnings(NotFound), батч применяется частично. Состояние: Delisted-карточки → Warnings(Delisted), не пересобираются (INV-7 property-object-card / делистинг project-card — удалённый источником объект не воскрешается ручным rebuild-ом). Итог: RequestedCount (уникальные) = RebuiltCount + число уникальных id в Warnings(NotFound|Delisted).
  3. Что читаем (батч по id, порядок: карточка → её оверрайды/правила/покупки → родительский проект):
    • TargetKind=PropertyObject: карточки объектов недвижимости по Ids (отсутствующие → Warnings); на каждую — живой оверрайд объекта недвижимости, живой оверрайд её корпуса, карточку её проекта; активные правила ранжирования объекта недвижимости/проекта и активные покупки продвижения, затрагивающие карточку.
    • TargetKind=Project: карточки проектов по Ids; на каждую — живые оверрайды корпусов проекта, объекты недвижимости проекта в статусе Published (для агрегатов), активное правило ранжирования и активные проектные покупки.
  4. Правила и переходы (единственная функция пересчёта — та же, что у консюмеров и мутаций оверрайдов; маппинг поле-в-поле):
    • Эффективные поля: Effective = Override(поле) ?? SourceSnapshot(поле) (INV-3 property-object-card; корпусной ComplexName → Name для project-card); PricePerMeter = round(Price / AreaTotal, 2).
    • Видимость: Status по INV-1 (PropertyObjectStatus != Hidden ∧ оверрайды IsVisible != false ∧ проект Published); каскад скрытия проекта на объекты недвижимости — INV-5 project-card.
    • DeveloperPriority = Active-правило объекта недвижимости ?? Active-правило проекта ?? 0 (INV-5 ranking-rule).
    • PromotionBoost = Σ BoostWeight активных покупок, затрагивающих карточку (проектные ProjectBoost + объекты недвижимости PropertyObjectBoost с этим объектом недвижимости; INV-6 promotion).
    • ResetAnalyticsAsOf = trueSourceSnapshot.AnalyticsAsOf = null (метрики применит replay invest-analytics, guard AsOf > null пройдёт).
    • Фиксируются доменные события: объект недвижимости → PropertyObjectCardRecomputedEvent (Id, ProjectId, Price, Status, PropertyObjectStatus), обработчик в том же коммите пересчитывает агрегаты проекта (MinPrice/MaxPrice/PropertyObjectsTotal/PropertyObjectsAvailable/Payback — INV-2 project-card); проект → ProjectCardRecomputedEvent (Id, Status, MinPrice, MaxPrice, PropertyObjectsAvailable) + прямой пересчёт агрегатов по Published-объектам недвижимости.
  5. Что меняется. Обновляются все пересчитанные карточки и затронутые родительские project_cards. RebuiltCount/RebuiltIds = карточки, дошедшие до обновления. Правка не должна затирать чужую (конкурентный доступ): конфликт с конкурирующим консюмером → CONCURRENCY_CONFLICT, retry всей операцией с тем же Idempotency-Key (пересчёт идемпотентен и детерминирован).
  6. События. Доменные PropertyObjectCardRecomputedEvent/ProjectCardRecomputedEvent — фиксируются в том же коммите. Интеграционные события не публикуются (витрина — конечная точка данных; обоснование — catalog-property-object-card.md, раздел «События»).
  7. Согласованность. Все пересчитанные карточки батча + пересчитанные агрегаты проектов + отметка идемпотентности + deep-audit — в одном согласованном изменении (весь батч — одна транзакция, ≤ MaxBatchSize строк; simplicity-first ADR-0056). Конфликт версий откатывает батч целиком.
  8. Результат. RebuildProjectionsResult (200; RebuiltCount, RebuiltIds, Warnings — консистентно транзакции); отказ → ProblemDetails с кодами таблицы ниже.

Диаграмма

Побочные эффекты

ТипДетали
Проекции / поискпересчёт catalog.property_object_cards/catalog.project_cards (эффективные поля, Status, DeveloperPriority, PromotionBoost, агрегаты, NameVector — generated column) в одной транзакции; при ResetAnalyticsAsOf — обнуление SourceSnapshot.AnalyticsAsOf (доедет replay-ем)

Интеграционные события не публикуются; внешних вызовов нет (пересчёт из локальных SourceSnapshot + оверрайдов/правил/покупок, в источники не ходит); уведомлений нет.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400TargetKind вне enum / Id = Guid.EmptyОшибка валидации запросаno retry
CATALOG_REBUILD_EMPTY_BATCH400Ids пустСписок id для пересчёта пустno retry
CATALOG_REBUILD_BATCH_TOO_LARGE400Ids.Count > MaxBatchSizeПревышен размер батча пересчётаno retry (разбить батч)
UNAUTHORIZED401Нет/невалиден m2m-токен или X-Admin-User-IdТребуется авторизацияno retry
FORBIDDEN403Клиент не pin-admin-system / нет catalog.overrides.manageДоступ запрещёнno retry
CONCURRENCY_CONFLICT409Гонка версий с консюмером/вторым админомДанные изменены параллельно; повторитеretry с тем же Idempotency-Key
Частичное применение батча — не ошибка

Отсутствующие (NotFound) и Delisted id возвращаются в Warnings при 200 — это штатный сценарий runbook-сверки, а не отказ запроса.

Совместимость и наблюдаемость

  • Новые optional поля запроса/ответа — additive (без breaking change); удаление/переименование/смена типа — breaking (migration + rollout). RebuildTargetKind/RebuildWarningReason — enum строкой, unknown-значение запроса → ValidationError.
  • Logs: admin-projections-rebuild (TargetKind, RequestedCount, RebuiltCount, число warnings, ResetAnalyticsAsOf, adminUserId; сами id — в audit, не в plain-логе); metrics: catalog_admin_operations_total{operation="rebuild-projections"}, duration, catalog_projection_recompute_total{trigger="rebuild"}; traces: span приёма → span обработки → spans обращений к хранилищу; correlation — сквозной trace через traceparent. Каждый вызов — в deep audit. Dashboard catalog-overview; runbook — сверка/rebuild проекций по id при расхождении SourceSnapshot.ChessboardVersion.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы