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

Event Contract: pin.news.comment-added (CommentAddedKafkaEvent)

Версионируемый контракт интеграционного события «добавлен видимый комментарий». Consumer-документы ссылаются сюда, не переописывая payload. Payload денормализован под адресацию уведомлений: несёт идентификаторы адресатов (автор поста, автор родительского комментария, автор комментария) без обращения потребителя обратно в news.

Назначение

Фиксирует создание видимого (Visible) комментария или ответа под опубликованным постом ленты (../domain/comment.md, переход [*] → Visible). Единственный downstream-потребитель — notifications, который по событию уведомляет автора поста («ваш пост прокомментировали») и автора родительского комментария («вам ответили»), не запрашивая news синхронно. Тело комментария (Body) в событие не входит — это UGC/PII; при необходимости потребитель читает его через публичное API news.

Metadata

ПолеЗначение
Event typepin.news.comment-added
Topicpin.news.comment-added.v1
Partitions / key3 / key = PostId (= AggregateId; порядок комментариев per-post)
ContractVersion1
Producernews: POST /api/v1/posts/{id}/comments (../api/public/comments/add-comment.md)
Статусdraft

Событие публикуется атомарно с созданием агрегата Comment: до фиксации изменения событие не отправляется, после — доставляется гарантированно (at-least-once). Kafka key = PostId — все комментарии одного поста попадают в одну партицию и упорядочены.

Payload CommentAddedKafkaEvent

Дом полей комментария — entity: Comment; поля поста (Suffix, AuthorUserId) — entity: Post. Здесь перечислено только то, что реально в событии; денормализованные поля адресатов включены осознанно, чтобы notifications не делал синхронный запрос в news.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
CommentIdGuidДаComment.IdImmutableИдентификатор комментария; ключ бизнес-идемпотентности у потребителя.
PostIdGuidДаComment.PostId (= Post.Id)ImmutableПост-владелец обсуждения; = Kafka key (порядок per-post).
PostSuffixstringДаPost.SuffixAdditive onlyURL-slug поста (/news/{suffix}, INV-NWS-001) для deep-link в уведомлении; денормализован.
PostAuthorUserIdGuidДаPost.AuthorUserIdImmutableАвтор поста — адресат уведомления «ваш пост прокомментировали».
ParentCommentIdGuid?НетComment.ParentCommentIdImmutablenull — корневой комментарий; иначе идентификатор родителя (ответ).
ParentCommentAuthorUserIdGuid?НетComment(Parent).AuthorUserIdImmutableАвтор родительского комментария — адресат «вам ответили»; null у корневого.
CommentAuthorUserIdGuidДаComment.AuthorUserIdImmutableАвтор нового комментария (актор); используется для дедупликации адресатов (себе не уведомляем).
CreatedAtDateTimeOffsetДаComment.CreatedAtImmutableМомент создания комментария.

Body (текст комментария) в payload не входит: UGC/PII — потребитель читает его при необходимости через публичное API news (GET /api/v1/posts/{id}/comments), а не из события (правило безопасности ниже). RootCommentId/Depth/Path также исключены — для адресации уведомлений не нужны; дом дерева — ../domain/comment.md.

Когда публикуется

УсловиеПереход состоянияСогласованность
Создание видимого комментария (корневого или ответа) — операция add-comment, инварианты INV-CMT-001..003[*] → Visible (../domain/comment.md)Comment, инкремент Post.CommentsCount и факт публикации события фиксируются в одном согласованном изменении

Скрытие/удаление комментария (HiddenByModerator/DeletedByAuthor) не публикует событие — терминальные переходы обрабатываются локально (INV-CMT-005: декремент CommentsCount), уведомлять некого.

Потребители (consumers)

Consumer-контекстЭффектИдемпотентностьЗадержка (SLA)
notificationsУведомления адресатам (см. правило ниже): «ваш пост прокомментировали» — PostAuthorUserId; «вам ответили» — ParentCommentAuthorUserIdCommentId (business key) + идентификатор сообщения< 1 мин

Правило адресации и дедупликации (в потребителе):

  • множество адресатов = { PostAuthorUserId, ParentCommentAuthorUserId } минус { CommentAuthorUserId } (автор не уведомляется о собственном комментарии);
  • если PostAuthorUserId == ParentCommentAuthorUserId — одно уведомление (дедуп по получателю; тип — «вам ответили» как более специфичный);
  • ParentCommentAuthorUserId == null (корневой комментарий) — уведомляется только PostAuthorUserId;
  • идемпотентность per получатель — по паре (CommentId, RecipientUserId): повторная доставка события не создаёт дубль уведомления.

Версионирование и совместимость

  • ContractVersion = 1; при несоответствии версии события consumer логирует warning и продолжает (нет upcasting/schema-migration). Топик кодирует версию по конвенции — суффикс .v1.
  • Additive-only для minor: новые optional-поля с безопасным default — без bump ContractVersion.
  • Breaking (удаление/переименование/смена типа или смысла поля) — ContractVersion = 2 + новый топик pin.news.comment-added.v2 + dual-publish на период миграции + deadline миграции consumer + sunset .v1.
  • Порядок per-post гарантирован ключом = PostId; ответ в треде публикуется после корня (CommentId хронологически монотонен), потребитель обрабатывает события одного поста последовательно.

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

ПолеКласс данныхПравило
CommentId, PostId, ParentCommentIdInternalinclude (идемпотентность/адресация)
PostSuffixPublicinclude (публичный slug опубликованного поста)
PostAuthorUserId, ParentCommentAuthorUserId, CommentAuthorUserIdInternalinclude (адресация уведомлений); в логах producer/consumer — маскирование id
CreatedAtInternalinclude
Body (текст комментария)PII / UGCexclude — в payload не входит; потребитель читает через публичное API news при необходимости

Событие НЕ содержит secrets, tokens, raw credentials, полные PII, платёжных данных и unbounded-вложений. Адресаты уведомлений — пользователи (User); tenant не применяется — Comment не tenanted (публичный UGC под опубликованным постом, ../domain/comment.md); доступ к топику — m2m ACL Kafka (producer news, consumer notifications).

Гарантии доставки и порядок

  • At-least-once, атомарно с агрегатом. Событие уходит только после фиксации создания Comment (транзакционный outbox): дубли доставки возможны, потерь нет; потребитель дедуплицирует по CommentId (шаг адресации выше). Окно между фиксацией и доставкой — секунды (лаг outbox → SLA потребителя < 1 мин); в этом окне уведомление ещё не отправлено, но комментарий уже виден в дереве поста (read-путь news не ждёт события).
  • Порядок per-post гарантирован ключом PostId (одна партиция на пост): для одного поста корневой комментарий доставляется раньше ответа на него (CommentId v7 хронологически монотонен). Между разными постами порядок не гарантирован — потребителю он не нужен (адресация независима на пост).
  • Что видит адресат в промежуточном состоянии. Пока событие в пути, автор поста уже может увидеть комментарий, открыв пост, но push/ин-апп уведомления ещё нет — это ожидаемая eventual-семантика, а не рассинхрон.

Acceptance Criteria

  • Корневой комментарий. Дано опубликованный пост с CommentsEnabled = true и автором A; когда пользователь B (B ≠ A) добавляет корневой комментарий; тогда публикуется одно событие с ParentCommentId = null, ParentCommentAuthorUserId = null, PostAuthorUserId = A, CommentAuthorUserId = B, и уведомляется только A.
  • Ответ в треде. Дано корневой комментарий автора C под постом автора A; когда B отвечает на него; тогда событие несёт ParentCommentAuthorUserId = C, PostAuthorUserId = A, и уведомляются A и C (два адресата).
  • Дедуп адресатов. Дано пост автора A, где A же оставил корневой комментарий; когда B отвечает автору A; тогда PostAuthorUserId == ParentCommentAuthorUserId == A → одно уведомление типа «вам ответили».
  • Комментарий к своему посту. Дано пост автора A; когда A комментирует собственный пост корневым комментарием; тогда множество адресатов {A} \ {A} пусто — уведомление не отправляется.
  • Повторная доставка. Дано событие с CommentId = X уже обработано notifications; когда то же событие доставлено повторно; тогда по идемпотентности (CommentId, RecipientUserId) новых уведомлений не создаётся.
  • Скрытие/удаление. Дано видимый комментарий; когда он переходит в HiddenByModerator или DeletedByAuthor; тогда событие comment-added не публикуется (уведомлять некого) — публикуется только при [*] → Visible.
  • Deep-link. Событие несёт PostSuffix, достаточный для сборки ссылки /news/{suffix} без обратного запроса в news.

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

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

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