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

GET /api/v1/projects — Лента проектов

Назначение

Лента карточек проектов (ЖК и апарт-отели) для главной страницы и /hotels: карточки с минимальным набором — название, адрес, девелопер, диапазон цен, количество объектов, окупаемость; полнотекстовый поиск по названию/адресу/девелоперу, фильтры тип/класс/регион/город/статус строительства/ценовой диапазон, сортировки цена/дата/популярность/окупаемость и дефолт «Рекомендованные», бесконечная подгрузка по 20–30 карточек. Доступ без гейта: анонимный гость видит ленту с урезанными полями — точные цены и статусы объектов скрыты (правила MASK-PRICE/MASK-STATUS — README.md). Источник — витрина catalog-project-card.

Metadata

ПолеЗначение
Сервис/контекстcatalog
API areapublic
Feature groupfeed
Статусdraft

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

ПроверкаЗначение
Actor typeGuest (маскированная лента) / RefGuest, User (полные поля)
Auth policy / Permissionsnone (public read; маскирование вместо отказа)
Scopeплатформа
Tenant isolationне применяется (платформенная витрина)
Auditnone; поиск/фильтры — фронт пишет в user-activity

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/projects
Success status200
Idempotencyне применяется (safe GET)
CorrelationOpenTelemetry trace (traceparent)

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorClassidentity middlewareGuest/RefGuest/UserДаневалидный токен → 401; Guest допустимREADME.md
SearchTermquerystring?Нетtrim; 1..256; передаётся параметром в полнотекстовый поиск (не конкатенация)untrusted; лог — 64 симв.
KindqueryProjectKind?Нетenum ResidentialComplex/ApartHotel (лента /hotelsKind=ApartHotel); иное → ValidationError
PropertyClassqueryPropertyClass?Нетenum Comfort/Business/Premium/Elite
RegionIdquerylong?Нет> 0; ключ фильтра «регион» — ссылка на справочник handbook (значения — lookup get-catalog-filters.md); несуществующий id — пустая выдача, не ошибка
CityIdquerylong?Нет> 0; ключ фильтра «город» (значения — тот же lookup); несуществующий id — пустая выдача
ConstructionStatusqueryConstructionStatus?Нетenum Planned/InProgress/Commissioned
Pricequery (priceMin/priceMaxPrice.Min/Price.Max)NullableRange<decimal>Нетзаданные границы > 0; well-formed Price.Min <= Price.Max; null-граница = открытый интервал; сопоставляется с диапазоном цен проекта (пересечение отрезков)
SortqueryFeedSortНетenum (README.md); default Recommended; Price = по MinPrice, Payback = по Payback проекта
Descquerybool?Нетdefault по сортировке; Recommended — фикс. desc
LimitqueryintНет1..30, default 24 (лента 20–30)
Cursorquerystring?Нетbase64url FeedCursorModel (README.md)не секрет

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

ПолеТипОбязательностьИсточникMaskingОписание
itemsIReadOnlyList<ProjectListItemModel>Дапроекция витрины проектовсм. itemСтраница карточек проектов.
nextCursorstring?НетcomputedКурсор следующей страницы; null — конец ленты.
totalCountlongДасчётчик по фильтруВсего проектов под фильтром.
appliedFiltersAppliedProjectFiltersModelДаecho запросаEcho для URL-состояния (шаринг).

ProjectListItemModel (минимальный набор ленты; источники — дом ../domain/catalog-project-card.md)

ПолеТипОбязательностьИсточникMaskingОписание
idGuidДаCatalogProjectCard.IdПроект (= id в chessboard).
namestringДаName (effective)Название комплекса/отеля.
kindstring (enum ProjectKind)ДаKindЖК / апарт-отель.
propertyClassstring (enum PropertyClass)ДаPropertyClassКласс.
regionId / regionNamelong / stringДаRegionId/RegionNameРегион (id справочника handbook + display-имя; у Published-проектов гео резолвлено — INV-PRJ-3 источника).
cityId / cityNamelong / stringДаCityId/CityNameГород (id справочника handbook + display-имя).
addressstringДаAddress (effective)Адрес.
developerDeveloperRefModelДаDeveloperId/DeveloperName/DeveloperLogoFileIdДевелопер (ref-модель — README.md; лого — пресайн storage/CDN).
coverFileIdGuid?НетCoverFileIdОбложка карточки.
shortDescriptionstring?НетShortDescriptionПодсказка при наведении.
constructionStatusstring (enum ConstructionStatus)ДаConstructionStatusСтатус строительства.
deliveryDatestring (ISO 8601 date)?НетDeliveryDate (effective)Срок сдачи.
readinessPercentshort?НетReadinessPercent (effective)Готовность.
starsshort?НетStars (effective)Звёздность отеля.
distanceMinutesint?НетDistanceMinutes (effective)Минуты до ключевой точки.
priceRangeNullableRange<decimal>ДаMinPrice/MaxPriceMASK-PRICE (Guest: min↓/max↑ до 100 000 ₽)Диапазон цен ленты (ref-модель — README.md).
pricePerMeterFromdecimal?Нетзарезервировано null в v1MASK-PRICE (Guestnull)Задел под «цена от … ₽/м²» (additive, появится после расширения агрегатов проекции).
propertyObjectsTotalintДаPropertyObjectsTotalВсего объектов (количество объектов ленты).
propertyObjectsAvailableint?НетPropertyObjectsAvailableMASK-STATUS (Guestnull)Доступные объекты (PropertyObjectStatus = Free).
paybackPaybackModel?Нетowned PaybackОкупаемость проекта (мин. набор ленты — видна и гостю).
growthPercentdecimal?НетGrowthPercentРост цены, %/год.
ratingdecimal?НетRatingРейтинг проекта.
geoLocationGeoLocationModel?Нетowned GeoLocationКоординаты для карты.

AppliedProjectFiltersModel

ПолеТипОбязательностьИсточникMaskingОписание
searchTermstring?Нетecho
kindstring?Нетecho
propertyClassstring?Нетecho
regionIdlong?Нетecho
cityIdlong?Нетecho
constructionStatusstring?Нетecho
priceNullableRange<decimal>НетechoПрименённый ценовой диапазон (сериализуется в query как priceMin/priceMax).
sortstring (enum FeedSort)Даecho (после default-ов)
descboolДаecho (после default-ов)

Алгоритм

  1. Контекст и доступ. Актор определяется по токену (User; невалидный → 401), затем по реф-cookie referralUrl (RefGuest), иначе Guest — лента доступна, маскирование вместо гейта.
  2. Проверка входа. Enum-ы/диапазоны/паттерны по таблице входа, Price well-formed (обе границы заданы → Price.Min <= Price.Max), Limit ∈ 1..30 — иначе ValidationError (400). Курсор проверяется как в get-property-objects.md шаг 2 → битый/несовместимый курсор даёт CATALOG_CURSOR_INVALID (400). Отбор — жёстко по опубликованным (Status = Published).
  3. Что читаем. Страница карточек проектов под фильтром: SearchTerm — полнотекстовый поиск по названию/адресу/девелоперу; фильтры Kind/PropertyClass/RegionId/CityId/ConstructionStatus; ценовой диапазон — пересечение отрезка запроса с диапазоном цен проекта; keyset-предикат из курсора и сортировка по выбранному ключу (по умолчанию — «Рекомендованные»). Отдельно — общий счётчик под фильтром без keyset.
  4. Сборка ответа и маскирование. Маппинг полей — таблица ProjectListItemModel. Для Guest: MASK-PRICE (priceRange округляется вниз/вверх до 100 000 ₽, при null остаётся null) и MASK-STATUS (propertyObjectsAvailable = null). Дефолт без явного выбора — Recommended (формула ранжирования, финальный ключ — дата публикации). nextCursor — из последней строки, если выбран лишний элемент сверх лимита.
  5. Результат. GetProjectsQueryResult → 200; appliedFilters — источник URL-состояния. Консистентность — eventual (агрегаты MinPrice/PropertyObjectsAvailable пересчитываются консюмерами — INV-2 project-card).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Передан невалидный/просроченный токенбез тела схемыre-login
ValidationError400Нарушение ограничений входаСписок ошибок полейno retry
CATALOG_CURSOR_INVALID400Битый/несовместимый курсор«Курсор устарел — обновите ленту»retry без cursor
Пустая выдача — не ошибка

200 с items = [] — валидный ответ.

Acceptance Criteria

  • Лента гостю. Дано Guest и опубликованные проекты. Когда запрашивает /api/v1/projects. Тогда 200, карточки с округлённым priceRange (MASK-PRICE) и propertyObjectsAvailable = null (MASK-STATUS); гейта нет.
  • Дефолт «Рекомендованные». Дано Sort не задан. Когда формируется страница. Тогда сортировка Recommended (формула ранжирования, финальный ключ — дата публикации), направление фиксировано desc.
  • Поиск и фильтры. Дано searchTerm + kind=ApartHotel + ценовой диапазон. Когда выполняется запрос. Тогда возвращаются только ApartHotel с пересечением ценового отрезка; totalCount — под фильтром.
  • Фильтр «регион»/«город». Дано regionId/cityId из lookup-а фильтров. Когда выполняется запрос. Тогда возвращаются только проекты с этим RegionId/CityId; несуществующий id даёт 200 с items = [] (не ошибка); regionId <= 0 или cityId <= 0ValidationError (400).
  • Пагинация. Дано первая страница вернула nextCursor. Когда клиент запрашивает со cursor. Тогда следующая страница без дублей в пределах той же сортировки; конец ленты → nextCursor = null.
  • Битый/несовместимый курсор. Дано cursor от другой сортировки или версии. Когда приходит запрос. Тогда CATALOG_CURSOR_INVALID (400); клиент повторяет без cursor (первая страница).
  • Стабильность при переранжировании. Дано во время подгрузки консюмер/джоб изменил PromotionBoost/PopularityScore/DeveloperPriority элемента. Когда клиент листает Recommended. Тогда keyset исключает дубли при неизменных ключах, но сдвиг ранга элемента между страницами может привести к его пропуску/повторному показу — принятое ограничение бесконечной ленты (обновление ленты снимает расхождение).
  • Пустая выдача. Дано фильтр без совпадений. Когда запрос выполнен. Тогда 200 с items = [].

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

  • Новые optional-поля item — additive (pricePerMeterFrom заложен null-ом); раскрытие маскированных полей гостю — breaking (MASK-* в README.md).
  • Logs/metrics/traces — общие правила area; дополнительно разрез метрик по kind (ЖК vs отели) и actor_class (доля маскированных ответов).

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

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

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