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

POST /api/developers/v1/ranking-rules — Создание правила ранжирования

Назначение

Менеджер застройщика создаёт правило приоритета показа своего проекта или объекта недвижимости в рамках своих объектов (ranking-rule.md). Правило создаётся сразу Active; значение Priority в той же транзакции денормализуется в DeveloperPriority затронутых карточек (INV-4 RecomputeInTx) и начинает действовать в сортировке «Рекомендованные» (формула — ranking-rule.md; на явные сортировки пользователя не влияет).

Metadata

ПолеЗначение
Сервис/контекстcatalog
API areadevelopers
Feature groupranking
Статусdraft
Документdocs/06-services/catalog/api/developers/create-ranking-rule.md

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

ПроверкаЗначение
Actor typeUser (менеджер застройщика — JWT identity)
Auth policy / Permissionspermission catalog.ranking.manage в scope организации/группы проектов/проекта (Scoped-RBAC)
Scope (RBAC)организация и ниже; при scope «проект» цель обязана входить в scope-проекты актора
Tenant isolationTenantId = CompanyId застройщика (ADR-0052); дополнительно INV-2 OwnObjectsOnly — карточка цели имеет DeveloperId == TenantId
Auditcreate: actor-stamping CreatedByUserId + Version = 1; история — soft-delete-модель

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/developers/v1/ranking-rules
Success status201
Idempotency headerне применяется (естественная идемпотентность: повтор упрётся в INV-1 CATALOG_RANKING_RULE_DUPLICATE)
Correlationсквозной trace через traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired (= CompanyId)tenant isolation
TargetKindbodystring (enum RankingTargetKind)Да∈ {Project, PropertyObject}
TargetIdbodyGuidДа!= Guid.Empty; существование и владение — шаг 2 (INV-2)
PrioritybodyintДа0..1000 (INV-3 PriorityRange)

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаDeveloperRankingRule.IdСозданное правило.
TargetKindstring (enum RankingTargetKind)ДаDeveloperRankingRule.TargetKind
TargetIdGuidДаDeveloperRankingRule.TargetId
PriorityintДаDeveloperRankingRule.Priority
Statusstring (enum RankingRuleStatus)ДаDeveloperRankingRule.StatusActive.
CardsRecomputedintДасчётчик пересчитанных карточекСколько карточек получили новый DeveloperPriority.
VersionlongДаDeveloperRankingRule.Version1.

Алгоритм

  1. Контекст и доступ. Актор — менеджер застройщика (JWT + permission catalog.ranking.manage); TenantId из claims. Идемпотентность не применяется.
  2. Проверка входа. TargetKind ∈ enum и TargetId != Guid.Empty — нарушение → ValidationError; Priority вне диапазона 0..1000 (INV-3 PriorityRange) → CATALOG_RANKING_PRIORITY_OUT_OF_RANGE (400). Существование и владение (INV-2 OwnObjectsOnly): карточка цели (проект или объект недвижимости по TargetKind) существует; нет → CATALOG_RANKING_TARGET_NOT_FOUND (404); DeveloperId != TenantIdCATALOG_RANKING_TARGET_NOT_OWNED (403); Status = DelistedCATALOG_RANKING_TARGET_NOT_FOUND (404 — удалённая цель неотличима от несуществующей для пользователя). Дубль (INV-1 SingleRulePerTarget): живое правило на ту же цель уже есть → CATALOG_RANKING_RULE_DUPLICATE (409). Scope: при scope-назначении уровня «проект/группа проектов» — проект цели (TargetId для Project, проект объекта для PropertyObject) ∈ scope-проектов актора, иначе FORBIDDEN.
  3. Что читаем. Карточку цели. Для пересчёта каскада: при TargetKind=Project — объекты недвижимости проекта (кроме Delisted) и их активные правила объектов недвижимости (правило объекта перекрывает проектное — INV-5); при TargetKind=PropertyObject — карточку объекта недвижимости.
  4. Правила и переходы. Создаётся правило со статусом Active (ranking-rule.md, stateDiagram: [*] → Active), CreatedByUserId = актор, Version = 1; фиксируется RankingRuleChangedEvent (Id, TenantId, TargetKind, TargetId, Priority, Status). Каскад INV-5 (единственная точка расчёта): эффективный DeveloperPriority объекта недвижимости = Active-правило объекта ?? Active-правило его проекта ?? 0. Маппинг: TargetKind=Project → карточка проекта получает DeveloperPriority = Priority, и каждый объект недвижимости проекта без собственного Active-правила → Priority; TargetKind=PropertyObject → один объект недвижимости → Priority.
  5. Что меняется. Создаётся правило; обновляются затронутые карточки (счётчик → CardsRecomputed). Порядок: правило → карточки. Единственность страхуется unique partial index ux_ranking_rules_target (гонка двух создающих запросов → CATALOG_RANKING_RULE_DUPLICATE); правка не должна затирать чужую (конфликт с консюмером → retry).
  6. События. Только доменное RankingRuleChangedEvent (в том же коммите). Интеграционные события не публикуются — ранжирование потребляется только витриной (ranking-rule.md, «События»).
  7. Согласованность. Правило + карточки — в одном согласованном изменении (INV-4 RecomputeInTx).
  8. Результат. RankingRuleResult (201, Location: /api/developers/v1/ranking-rules/{id}); отказ → ProblemDetails.

Диаграмма

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

ТипДетали
Проекции / поискобновление DeveloperPriority в catalog.project_cards/catalog.property_object_cards (ленточный индекс ix_*_feed) в той же транзакции
Интеграционные события

Интеграционные события не публикуются; внешних вызовов и уведомлений нет.

Окно видимости эффекта

В ленте без поиска (сортировка Recommended на PostgreSQL-индексе ix_*_feed) новый DeveloperPriority виден сразу после коммита. В результатах поиска (searchTerm, Elasticsearch — ADR-0059) приоритет проявляется после синхронизации search-проекции карточек (окно свежести проекции — секунды); пользователь может краткий промежуток видеть прежний порядок в поиске при уже применённом приоритете в обычной ленте.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Неизвестное значение TargetKind / пустой TargetIdОшибка валидации запросаno retry
CATALOG_RANKING_PRIORITY_OUT_OF_RANGE400Priority вне 0..1000 (INV-3)Приоритет вне допустимого диапазонаno retry
UNAUTHORIZED401Нет/просрочен JWTТребуется авторизацияno retry
FORBIDDEN403Нет permission / цель вне scope-проектов актораДоступ запрещёнno retry
CATALOG_RANKING_TARGET_NOT_OWNED403card.DeveloperId != TenantId (INV-2)Объект не принадлежит вашей компанииno retry
CATALOG_RANKING_TARGET_NOT_FOUND404Карточка цели не существует или DelistedОбъект не найден в каталогеno retry
CATALOG_RANKING_RULE_DUPLICATE409Живое правило на цель уже есть (INV-1)Правило для этого объекта уже существуетno retry (использовать update)
CONCURRENCY_CONFLICT409Гонка версий карточек с консюмеромПовторите операциюretry

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

  • Новые optional поля — additive; удаление/переименование/смена типа — breaking (migration + rollout).
  • Logs: ranking-rule-created (ruleId, tenantId, targetKind, targetId, priority, cardsRecomputed); metrics: catalog_developers_operations_total{operation="create-ranking-rule"}, duration, catalog_projection_recompute_total{trigger="ranking"}; traces: span приёма → span обработки → span обращения к хранилищу; correlation — сквозной trace через traceparent. Dashboard catalog-overview.

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

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

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