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

News Service (Pin.News)

Назначение

news владеет пользовательским контентом платформы PIN: посты/новости/блоги в общую ленту и публикации, привязанные к автору, компании или проекту. Пост — блочный контент (типизированные jsonb-блоки: текст, галерея изображений, embed-подборка из selections) с категориями по типам авторов, вложенными переключаемыми комментариями и полным snapshot-версионированием истории публикаций (полный слепок, не diff). Предоставляет: CRUD постов и черновиков, publish/unpublish, редактирование блоков, ленты global и subscriptions, полнотекстовый поиск постов (Elasticsearch-проекция, ADR-0059), комментарии и подписки.

Metadata

ПолеЗначение
Статусdraft
Owning teamPIN Engagement
DomainВовлечение / Контент (engagement)
Data classificationPublic/Internal (опубликованные посты и комментарии публичны; черновики — internal владельца; PII нет — авторские ФИО берутся из реплики identity, не хранятся как первичные)
Runtime criticalitymedium (лента контента; недоступность не блокирует каталог/сделки, но заметна пользователям)

Ownership

ArtifactOwner/Rule
PostgreSQL schemanews
API areaspublic (ленты global/subscriptions, чтение поста, комментарии, подписки, справочник категорий), authoring (черновики/публикация/блоки/версии автора — self + company scope), admin (категории, модерация — m2m, ADR-0053)
Owned entitiesPost, PostBlock (typed jsonb на Post), Comment, PostVersion, PostCategory, AuthorSubscription; проекции-реплики AuthorReplica, CompanyReplica (read-модели из identity); отложенные PostRankingSignals/PostReaction (спроектированы, реализация отложена)
Produced eventspin.news.post-published.v1, pin.news.comment-added.v1
Consumed eventspin.identity.users.v1, pin.identity.companies.v1, pin.promotion.purchase-changed.v1 (буст постов, ADR-0060)
Синхронные интеграцииselections internal API (create/detach/get SelectionSnapshot для embed-блока, m2m); catalog (display-данные объектов недвижимости при рендере — вызывает фронт/рендер, не бэкенд)
Search indexesElasticsearch-проекция news-posts (PostSearchDocument, Talent.Common.Domain.Search, ADR-0059; search/posts.md) — только Published-посты; PostgreSQL-FTS (tsvector/GIN) не используется
Analytics factsnone (ClickHouse — только dev-stats, ADR-0056/0057; просмотры постов трекает фронт → user-activity ActionEvent, ADR-0054)

Ответственность И Границы

  • Делает: черновики и жизненный цикл поста (Draft/Published/Unpublished/Archived); редактирование блоков (text/gallery/selection-embed) как typed-jsonb коллекции; иммутабельную копию подборки в пост через синхронный вызов selections (хранит SnapshotId в блоке; замена блока = новый snapshot + detach старого); категории по типам авторов (PostCategory.AllowedAuthorTypes + permission identity); полное snapshot-версионирование при каждой публикации; вложенные переключаемые комментарии; подписки и ленту subscriptions; глобальную ленту + поиск/фильтры/сортировки; денормализованные авторские/компанийные подписи (реплики identity — лента без синхронных джойнов).
  • НЕ делает: source of truth пользователей/компаний (identity); подборки и их иммутабельные копии (selections — владеет Selection/SelectionSnapshot, INV-SEL-001; news лишь ссылается по SnapshotId); display-данные объектов недвижимости embed-блока (батч рендера в catalog, цены не заморожены — показываются актуальные); трекинг просмотров/лайков как событий (фронт → user-activity, ADR-0054); доставку уведомлений подписчикам (notifications по событию post-published); медиа-хранилище (storage/CDN — FileId, пресайн-ссылки, без FK); ранжирование/рекомендации (спроектировано, реализация отложена — решение №10).
  • Source of truth: Post, Comment, PostVersion, PostCategory, AuthorSubscription.
  • Реплики/проекции: AuthorReplica/CompanyReplica — реплики identity; embed-блок — ссылка на SelectionSnapshot (source of truth — selections).

Зависимости

ЗависимостьТипЗачем НужнаFailure mode
PostgreSQL (schema news)dbВсе таблицы (source of truth) + outboxПолный отказ сервиса
KafkaqueueProduced pin.news.* (outbox), consumed pin.identity.*CRUD/ленты работают; свежесть авторских подписей и рассылка подписчикам деградируют (outbox/консюмеры догонят)
Elasticsearchservice (search projection, Talent.Common.Domain.Search)Полнотекстовый поиск постов (news-posts, ADR-0059)Поиск по searchTerm деградирует/недоступен; лента без searchTerm, чтение поста и авторство работают (проекция rebuildable из PostgreSQL)
selectionsservice (internal API m2m, синхронно)Снятие/detach/чтение SelectionSnapshot для embed-блокаДобавление/замена блока-подборки блокируется (NEWS_SELECTIONS_UNAVAILABLE); прочее авторство работает; рендер уже опубликованных блоков читает snapshot по SnapshotId
identityservice (JWT/permissions + события)Аутентификация авторов/комментаторов; permission news.*; snapshot автора/компанииПубличное чтение лент/постов живёт; авторство/комментарии — нет; подписи из реплик (staleness при лаге)
catalogservice (используется фронтом/рендером, не бэкендом)Актуальные display-данные объектов недвижимости embed-блокаБлок-подборка показывает PropertyObjectId без карточек
promotionservice (Kafka consumer, ADR-0060)Буст продвижения постов (pin.promotion.purchase-changed.v1Post.PromotionBoost)PromotionBoost не обновляется (замирает на последнем снапшоте); лента/ранжирование работают
Администратор платформы (admin API)consumer (m2m, ADR-0053)CRUD категорий, модерация постов/комментариевAdmin-правки недоступны

Redis/ClickHouse не используются (ADR-0056).

Проекты

src/services/news/
Pin.News.Api/ # public + authoring + admin API areas
Pin.News.Application/ # обработка операций; search-запросы (Elasticsearch); snapshot-версионирование; оркестрация selections-клиента
Pin.News.Domain/ # Post, PostBlock (jsonb), Comment, PostVersion, PostCategory, AuthorSubscription, реплики, enums, инварианты
Pin.News.Infrastructure/ # хранилище PostgreSQL, Elasticsearch search-проекция, outbox, selections-клиент (m2m)
Pin.News.Background/ # consumers: identity, promotion-boost (Post.PromotionBoost); jobs: comment-count-reconcile, snapshot-detach-retry, post-search-rebuild (ranking-recompute — отложен)
Pin.News.Migrator/ # миграции схемы news + SEED PostCategory (категории по типам авторов)
Pin.News.Contracts/ # pin.news.* event contracts

Основные Сущности

EntityНазначениеSource Of TruthДокумент (целевой)
PostАгрегат поста: метаданные, категория, статус жизненного цикла, привязки автор/компания/проект, счётчикиnewsdomain/post.md
PostBlockTyped-jsonb блок (text/gallery/selection-embed) как коллекция на Post; embed хранит SnapshotIdnewsdomain/post.md
CommentВложенное переключаемое дерево комментариев (soft delete, модерация)newsdomain/comment.md
PostVersionImmutable snapshot поста при каждой публикации (полное версионирование)newsdomain/post-version.md
PostCategorySEED-справочник категорий по типам авторов (AllowedAuthorTypes)newsdomain/post-category.md
AuthorSubscriptionПодписка пользователя на автора (лента subscriptions)newsdomain/author-subscription.md
AuthorReplica / CompanyReplicaRead-проекции подписи автора/компании из identity (лента без джойнов)identity (реплика)domain/identity-replicas.md
PostRankingSignals / PostReactionСигналы ранжирования и реакции — спроектированы, реализация отложенаnewsdomain/post-ranking-model.md

ER-обзор

Жизненный цикл поста (первичная и повторная публикация пишут PostVersion):

API (карта областей; детали — api/<area>/...)

  • public: GET /api/v1/posts (глобальная лента, keyset-пагинация, поиск searchTerm через Elasticsearch, фильтры categoryId/authorType/projectId/authorCompanyId, сортировки recommended [deferred→date]/ date/popularity); GET /api/v1/feed/subscriptions (лента по подпискам, JWT); GET /api/v1/posts/{idOrSuffix} (чтение поста; view-трекинг — фронт→user-activity); GET /api/v1/posts/{id}/comments (дерево комментариев); POST /api/v1/posts/{id}/comments (добавить/ответить → comment-added); DELETE /api/v1/comments/{id} (soft, автор/модерация); POST|DELETE|GET /api/v1/subscriptions[/{id}]; GET /api/v1/categories (справочник + eligibility актора).
  • authoring (self + company scope; news.posts.create-* по scope автора + news.posts.manage-own): POST /api/v1/posts (черновик); PUT /api/v1/posts/{id} (метаданные: name/suffix/title/subtitle/ category/projectRefs); PUT /api/v1/posts/{id}/blocks (блоки целиком; embed → selections snapshot create/detach); POST /api/v1/posts/{id}/publish (→ PostVersion + post-published); POST /api/v1/posts/{id}/unpublish; DELETE /api/v1/posts/{id} (soft + detach snapshots); POST /api/v1/posts/{id}/comments-settings (переключить CommentsEnabled); GET /api/v1/authoring/posts (мои посты/черновики); GET /api/v1/posts/{id}/versions + GET /api/v1/posts/{id}/versions/{versionNumber}.
  • admin (m2m, ADR-0053; news.categories.manage, news.posts.moderate): CRUD PostCategory; модерация (скрыть пост/комментарий).

Events И Consumers

TypeNameTopicНазначение
ProducedPostPublishedKafkaEventpin.news.post-published.v1Уведомления подписчиков (notifications); таймлайн действий автора (user-activity, опц.)
ProducedCommentAddedKafkaEventpin.news.comment-added.v1Уведомление автора поста и автора родительского комментария (notifications)
ConsumedUserUpserted / UserBlockedpin.identity.users.v1Upsert AuthorReplica (подпись автора без джойна); blocked → скрытие
ConsumedCompanyUpserted / CompanySuspendedpin.identity.companies.v1Upsert CompanyReplica; suspended → скрытие постов компании
ConsumedPromotionPurchaseChangedpin.promotion.purchase-changed.v1Цели TargetKind = Post → пересчёт Post.PromotionBoost (Σ активных BoostWeight); consumers/promotion-boost.md, ADR-0060

Публикуются атомарно с мутацией агрегата; ключ = PostId/CommentId; ContractVersion = 1. Embed-подборки не через Kafka — синхронный API selections (решение №1).

Кросс-сервисные контракты и согласованность

Что news предоставляет и ожидает на границах, и какие окна eventual-consistency видит пользователь.

КонтрактНаправлениеГарантии / ключ / версияПоведение при отсутствии/устаревании
pin.news.post-published.v1предоставляет (async, at-least-once)ключ PostId, ContractVersion = 1; публикуется атомарно с публикацией; порядок публикаций одного поста сохранёнпотребитель (notifications/user-activity) недоступен — событие догоняет из outbox; дубликаты — на потребителе идемпотентно
pin.news.comment-added.v1предоставляет (async, at-least-once)ключ CommentId, ContractVersion = 1; атомарно с добавлениеманалогично; уведомление автора/родителя догоняет
pin.identity.users.v1ожидает (consumed)upsert AuthorReplica (подпись автора); blocked → скрытиереплики нет/лаг → подпись деградирует (fallback), авторство/чтение живут; окно лага < 1 мин (SLO)
pin.identity.companies.v1ожидает (consumed)upsert CompanyReplica; suspended → скрытие постов компаниилаг → устаревшее название компании до догона
pin.promotion.purchase-changed.v1ожидает (consumed, ADR-0060)цели TargetKind = Post → пересчёт Post.PromotionBoostнедоступно → PromotionBoost замирает на последнем снапшоте; лента/ранжирование работают
selections internal APIожидает (sync, m2m)create/detach/get SelectionSnapshot; detach идемпотентен (INV-SNP-005), snapshot иммутабелен (INV-SNP-006)недоступен → добавление/замена embed блокируется (NEWS_SELECTIONS_UNAVAILABLE); отвязка после коммита — надёжный повтор job-ом snapshot-detach-retry
Elasticsearch-проекция news-postsпредоставляет (внутр. проекция)только Published-посты; rebuildable из PostgreSQL (ADR-0059)недоступна → поиск по searchTerm деградирует; лента без поиска, чтение поста и авторство живут

Окна eventual-consistency (что видит пользователь в промежуточном состоянии):

  • Публикация: автор сразу видит пост Published в глобальной ленте (source of truth — news); подписчики получают уведомление и полнотекстовый поиск находит пост — с задержкой (лаг консюмера/проекции < 1 мин).
  • Правка метаданных/блоков Published-поста: чтение поста показывает новый состав сразу после коммита; поисковая проекция догоняет в окне секунд.
  • Снятие/удаление: пост выпадает из лент и чтения немедленно; из поиска — в окне проекции; отвязка embed-snapshot-ов (только при удалении) может выполниться позже job-ом, но исторические PostVersion всё это время корректно ссылаются на иммутабельные snapshot-ы.
  • Подписи автора/компании: eventual — устаревают на время лага консюмеров identity; идентичность (AuthorUserId/AuthorType) всегда точна (frozen в слепке).

Runtime И Эксплуатация

ОбластьТребование
Health checks/health/live; /health/ready (PostgreSQL, Kafka-producer, Elasticsearch, доступность selections — degraded, не fail); /health/startup (миграции + SEED категорий)
SLO/SLAлента p99 < 300 мс (keyset + индексы); поиск p99 < 400 мс (Elasticsearch); чтение поста p99 < 200 мс; лаг консюмеров identity < 1 мин; лаг search-проекции < 1 мин
Dashboardnews-overview (RPS лент, publish/comment rate, лаг консюмеров, DLQ, ошибки internal-вызовов selections, размер Archived)
Alertsрост 5xx selections-клиента (блокирует embed-публикации); DLQ > 0; outbox-lag > 5 мин; drift CommentsCount; лаг/ошибки Elasticsearch-индексатора (расхождение проекции news-posts)
Runbooksreplay DLQ консюмеров identity; застрявший outbox; «пост показывает не тот состав» (сверка SnapshotId блока ↔ snapshot в selections); ручной detach осиротевших snapshot-ов
Feature flagsnews.subscriptions.enabled; news.promotions.enabled (гейт консюмера буста постов); news.ranking.enabled (скрыт до готовности ранжирования)

Jobs (Pin.News.Background, по background-job-template): comment-count-reconcile (ночной пересчёт CommentsCount), snapshot-detach-retry (надёжный detach snapshot-ов удалённых/изменённых постов); ranking-recompute — отложен (решение №10).

Безопасность

  • Actor types: guest (лента global + чтение опубликованного поста и комментариев; подписки/ комментарии/авторство — нет), user (комментарии, подписки, авторство своих постов), developer/agency manager (авторство в scope компании + ProjectRefs), m2m admin (категории/модерация), system (консюмеры/jobs/selections-клиент).
  • Permission model: канонический каталог — identity/domain/permission.md: news.posts.view, news.posts.create-personal/create-company/create-project, news.posts.manage-own, news.posts.moderate, news.categories.manage. Eligibility публикации = actor.UserType ∈ Category.AllowedAuthorTypesnews.posts.create-* (решение №5). Комментарии/подписки — self-scope аутентифицированного пользователя (без отдельного permission).
  • Sensitive data classes: PII первичных нет — авторские ФИО в реплике; Comment.Body — публичный UGC (модерация через news.posts.moderate).
  • Audit: CreatedAt/UpdatedAt, actor-stamping мутаций, PostVersion как append-only история; soft-delete постов/комментариев.
  • Tenant isolation: ADR-0052 — TenantId = AuthorCompanyId для постов застройщика/агентства; посты инвестора — вне tenant (nullable); опубликованный контент для чтения — платформенного уровня (лента global публична); record-level мутаций — по AuthorUserId (+ company scope для менеджеров); тесты изоляции обязательны для authoring.

Ключевые Решения

  1. Embed-подборки — синхронный internal API selections (create/detach/get snapshot), не Kafka-консюмер. news не подписан ни на pin.selections.selection-shared (адресован crm/user-activity, не embed), ни на snapshot-created (не существует); связь PostId ↔ SnapshotId news держит в своей транзакции (UC-SEL-004).
  2. Иммутабельная копия подборки (INV-SEL-001): блок хранит SnapshotId; правка живой подборки владельцем блок не меняет; единственная правка автором поста — замена блока целиком (новый snapshot + Detach(BlockReplaced) старого, порядок «создать→переключить→отвязать»). Snapshot-ы иммутабельны и не удаляются (INV-SNP-006) → старые PostVersion рендерят исторический состав. Детали — selection-snapshot.md.
  3. Display-данные объектов недвижимости embed-блока не заморожены — рендер добирает актуальные из catalog по PropertyObjectId; заморожены только состав/порядок/комментарии подборки.
  4. Полное snapshot-версионирование (PostVersion), не diff: пишется при каждой публикации как immutable документ.
  5. Категории по типам авторов: PostCategory — SEED-справочник во владении news; застройщик привязывает пост к компании/проектам (AuthorCompanyId, ProjectRefs), агент — к себе.
  6. Ранжирование/рекомендации — спроектированы, реализация отложена (решение №10): проекция PostRankingSignals и рекомендатель — заглушки; сортировка recommended временно = date.
  7. Поиск — Elasticsearch-проекция (ADR-0059, supersedes FTS-пункт ADR-0056): PostSearchDocument через Talent.Common.Domain.Search (SearchQuery/SearchScopeFilter/ISearchRebuildJob); PostgreSQL-tsvector/GIN не используется. Прочее по ADR-0056 simplicity-first: PostgreSQL — source of truth, Redis/ClickHouse не используются; нагрузка ≤ 1 млн users, 1–10k активных.
  8. ADR-0052 tenancy / ADR-0053 admin / ADR-0054 (view-трекинг → user-activity, денормализованные ViewsCount/CommentsCount — только для отображения/сортировки, пересчёт reconcile-job-ом).

Известные Ограничения И Assumptions

  • Контракт блока поста (SnapshotId в typed-jsonb) согласован с selections (selection-snapshot.md, assumptions) — здесь финализируется.
  • Существование ProjectRefs/PropertyObjectId синхронно не проверяется (ADR-0056); недоступные объекты недвижимости помечает выдача catalog; несуществующий проект — мягкая деградация подписи.
  • Ранжирование не действует до реализации (решение №10) — recommended = date.
  • Модерация минимальная (скрытие); расширенный арбитраж — со support (отложено).
  • Свежесть авторских подписей — eventual (лаг консюмеров identity).

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

  • Соседи: selections/ (блок-подборка, snapshot) · catalog/ (display-данные объектов недвижимости) · identity/ (авторы/компании/permissions) · user-activity/ (view-трекинг) · crm/ · promotion/ (буст постов, ADR-0060); логические потребители событий — notifications, storage/CDN (ещё не спроектированы).
  • Решения: ADR 0052 (tenancy) / 0053 (admin) / 0054 (просмотры → user-activity) / 0056 (simplicity-first) / 0057 (dev-stats) / 0059 (Elasticsearch) / 0060 (promotionPromotionBoost); technical-assignment.md (№10), service-boundaries.md, glossary.md.