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

События сервиса news: карта топиков и отклонённые события/консюмеры

Индекс интеграционных событий pin.news.* (по одному контракт-доку на топик) + дом решений об интеграциях сервиса: что публикуется, что потребляется и что сознательно не публикуется/консюмится. Минимальность — ADR-0056: топик существует только при живом потребителе, которому синхронный API не подходит.

Metadata

ПолеЗначение
Owning teamPIN Engagement
Статус документаdraft
Область действияконтекст news, папка events/ (produced + consumed + отклонённые)
Assumptionscontract-доки post-published.md/comment-added.md, consumer-доки ../consumers/*.md — планируются; дом полей payload — доменные агрегаты
ЗависимостиKafka (outbox producer + консюмеры), identity (upstream-топики), selections (синхронный m2m вместо Kafka)

Метаданные и общие правила

Каждое событие несёт метаданные: MessageId (Guid v7, для дедупликации), CreatedAt, версию контракта, типизированный payload, AggregateId, AggregateType; сквозной trace восстанавливается из метаданных. Оба produced-топика публикуются через транзакционный outbox атомарно с мутацией агрегата. У обоих ContractVersion = 1.

Ключ партиционирования обоих топиков = PostId — гарантирует per-post порядок: публикация и комментарии одного поста упорядочены между собой. У обоих AggregateId = PostId (= Kafka key), AggregateType = "Post"; comment-added дополнительно несёт CommentId в payload (NumberOfPartitions = 3 у каждого топика).

Публикуемые события (по одному контракт-доку на топик)

СобытиеTopicProducer-операцияConsumersКонтракт
PostPublishedKafkaEventpin.news.post-published.v1POST /api/v1/posts/{id}/publish (Publish/RepublishPublished)notifications (рассылка подписчикам); user-activity (таймлайн действий автора, опц.)post-published.md
CommentAddedKafkaEventpin.news.comment-added.v1POST /api/v1/posts/{id}/comments (Add/ReplyVisible)notifications (автору поста + автору родительского комментария)comment-added.md

Дом полей payload — доменные агрегаты (post.md / comment.md, раздел «Доменные и интеграционные события»); ниже — типизированная проекция (дельта), а не второй дом.

Payload PostPublishedKafkaEvent

ПолеТипОбяз.Источник (Post)Описание
PostIdGuidДаIdКлюч агрегата (= Kafka key).
AuthorUserIdGuidДаAuthorUserIdАвтор поста (адресат уведомлений подписчиков).
AuthorTypestring (enum PostAuthorType)ДаAuthorTypeInvestor/DeveloperManager/Agent/AgencyManager/PinEmployee.
AuthorCompanyIdGuid?НетAuthorCompanyIdКомпания-автор; null — инвестор/сотрудник.
CategoryIdGuidДаCategoryIdКатегория поста.
SuffixstringДаSuffixURL-slug (kebab-case) для диплинка в уведомлении.
TitlestringДаTitleЗаголовок опубликованного поста.
Subtitlestring?НетSubtitleПодзаголовок.
CoverFileIdGuid?НетCoverFileIdОбложка (storage/CDN, без FK).
ProjectIdsIReadOnlyList<Guid>Да (может быть пустым)ProjectRefs.ProjectIdsПривязка застройщика к проектам.
VersionNumberintДаCurrentVersionNumberНомер PostVersion этой публикации.
PublishedAtDateTimeOffsetДаPublishedAtМомент текущей публикации.
FirstPublishedAtDateTimeOffsetДаFirstPublishedAtМомент первой публикации (republish не меняет).

Payload CommentAddedKafkaEvent

ПолеТипОбяз.Источник (Comment)Описание
CommentIdGuidДаIdИдентификатор добавленного комментария.
PostIdGuidДаPostIdПост-владелец (= AggregateId = Kafka key, per-post порядок).
ParentCommentIdGuid?НетParentCommentIdnull — корневой; иначе — адресат «ответили на ваш комментарий».
RootCommentIdGuid?НетRootCommentIdКорень треда.
AuthorUserIdGuidДаAuthorUserIdАвтор комментария (для anti-self-notify у notifications).
DepthshortДаDepthУровень вложенности (0..6).
CreatedAtDateTimeOffsetДаCreatedAtМомент создания.

Тело комментария (Body) в payload не входит — потребителю (notifications) не нужно, UGC читается по API поста; минимизация полезной нагрузки и поверхности данных.

Гарантии доставки и окна согласованности

Оба produced-топика — at-least-once, публикация атомарна с мутацией агрегата (транзакционный outbox): до фиксации изменения событие не уходит, после — доставляется гарантированно; дубли доставки возможны и дедуплицируются потребителем по бизнес-ключу (PostId+VersionNumber для публикации, CommentId для комментария). Порядок гарантирован только per-post (ключ = PostId).

ПотокЧто видит пользователь сразу (read-путь news)Асинхронное следствие и окно
Публикация постаПост в ленте global и по прямой ссылке /news/{suffix}сразу после коммита (частичный индекс по Status=Published)Лента subscription — pull-запросом подписчика, тоже сразу (события для неё не требуется); поисковая проекция news-posts (fts, ADR-0059) — через секунды (upsert по доменному событию); notifications рассылает подписчикам с SLA < 1 мин
Добавление комментарияКомментарий в дереве поста и инкремент CommentsCountсразу (в одной транзакции)notifications уведомляет адресатов с SLA < 1 мин; в окне доставки уведомления ещё нет, но комментарий уже виден
Реплика identity (consumed)Прежняя подпись автора/компании (последний применённый upsert)Новая подпись/статус — после обработки консюмером (лаг < 1 мин SLO); эффекты Blocked/Suspended (INV-REP-002/003) применяются query-слоем выдачи, а не в момент приёма события

Публикация без реального потребителя не заводится (ADR-0056): post-published/comment-added существуют, потому что у них есть notifications; отклонённые кандидаты — ниже.

Потребляемые события (consumed)

Реплики подписей identity для join-free ленты (дом — identity-replicas.md). Мутация реплики и отметка идемпотентности — в одном согласованном изменении.

Топик (upstream)СобытиеConsumer-докSide effectИдемпотентность
pin.identity.users.v1UserUpsertedKafkaEvent / UserBlockedKafkaEvent../consumers/identity-users.mdUpsert AuthorReplica (подпись автора без джойна); Blocked → пометка/скрытие постов автора (INV-REP-002)MessageId + монотонный SourceVersion-guard (INV-REP-001/004)
pin.identity.companies.v1CompanyUpsertedKafkaEvent / CompanySuspendedKafkaEvent../consumers/identity-companies.mdUpsert CompanyReplica; Suspended → исключение постов компании из публичных лент (INV-REP-003)то же

Контракты источника — user-upserted.md / user-blocked.md / company-upserted.md / company-suspended.md. Реплики собственных событий не публикуют (ADR-0056 — не плодить события без потребителей).

Отклонённые события/консюмеры (решение, не пробел)

pin.selections.selection-shared / snapshot-created — НЕ консюмятся

news не подписан на эти топики. snapshot-created selections не публикует (снятие snapshot — синхронный m2m); selection-shared selections публикует, но для crm/user-activity (лента лида / история действий агента) — к embed-подборкам поста отношения не имеет (selections/events/README.md). Embed-интеграция newsselectionsсинхронная через internal API selections (m2m: create/detach/get snapshot); связь PostId ↔ SnapshotId news держит в своей транзакции.

#ПричинаДетали
1Событие к embed не относится / отсутствуетselection-shared адресован crm/user-activity, не блоку поста; snapshot-created selections не эмитит — консюмить нечего (ADR-0056).
2Единственное взаимодействие синхронноnews вызывает POST /internal/selections/{id}/snapshots и получает SnapshotId в ответе (UC-SEL-004); событие ничего не добавило бы.
3Консистентность на синхронном ответеSnapshotId фиксируется в typed-jsonb блоке поста в транзакции news; событийная связка открыла бы окно рассинхрона.

Пересмотр: серверная операция selections с реальным событийным потребителем — additive.

pin.news.post-liked / post-unpublished / post-deleted — НЕ публикуются

КандидатПричина отклонения
post-likedЛайки моделируются PostReaction (спроектировано, реализация отложена — решение №10, ADR-0056) как часть отложенного ранжирования; внешнего потребителя нет (post-ranking-model.md).
post-unpublishedСнятие с публикации меняет Status в БД news; из ленты пост выпадает частичным индексом, из Elasticsearch-проекции поиска — внутренним доменным событием PostUnpublishedEvent (не Kafka, ADR-0059); notifications «отписку» не рассылает — потребителя факта нет.
post-deletedSoft-delete + detach snapshot-ов — синхронный вызов selections (UC-SEL-004), а не событие; внешнего потребителя нет.

Пересмотр — additive при появлении реального потребителя (запуск ранжирования, внешний индекс/кэш).

Просмотры постов — не событие news

Просмотр поста трекает фронтActionEvent в user-activity (ADR-0054), а не Kafka-событие news. Денормализованный Post.ViewsCount — только для отображения/сортировки, пересчёт reconcile-job-ом.

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

ПотокКласс данныхПравило
post-published payloadPublicОпубликованный пост публичен; include (secrets/PII в payload нет).
comment-added payloadInternalТолько id-граф комментария + Depth/CreatedAt; тело Body (UGC) исключено — exclude.
consumed identity-репликиPII (ФИО автора / название компании)Приходят из доверенного топика identity (m2m ACL); в репликах news — публичная display-подпись; PhoneNumber/Email не реплицируются; в логах ФИО маскируется.
AuthorUserId / AuthorCompanyId / id-поляInternalinclude.

Actor/permission — на стороне producer-операций (news.posts.*, гейт CommentsEnabled); консюмеры — system actor (без пользовательского контекста). События НЕ содержат secrets, tokens, raw PII/платёжных реквизитов, тел постов/комментариев целиком, unbounded-вложений.

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