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

DELETE /api/v1/comments/{id} — Удалить/скрыть комментарий

Терминальный переход комментария из Visible в DeletedByAuthor (автор) либо HiddenByModerator (оператор с news.comments.moderate). Узел остаётся плейсхолдером в дереве — ответы (дети) не «отрываются» (INV-CMT-006). Дом полей и инвариантов агрегата — comment.md.

Назначение

Дать автору убрать свой комментарий, а оператору — скрыть нарушающий UGC, не разрушая ветку обсуждения и денормализованный счётчик Post.CommentsCount.

Metadata

ПолеЗначение
Сервис/контекстnews
API areapublic
Feature groupcomments
Статусdraft
Документdocs/06-services/news/api/public/comments/delete-comment.md

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

ПроверкаЗначение
Actor typeUser (автор комментария) или Operator(Admin) (модерация, m2m — ADR-0053)
Auth policy / Permissionsаутентифицированный User (self record-level) или permission news.comments.moderate (permission.md)
Scope (RBAC)не применяется (комментарий — публичный UGC, Comment не tenanted)
Record-levelавтор: actor.UserId == Comment.AuthorUserId; оператор: permission-гейт news.comments.moderate (INV-CMT-004)
Tenant isolationне применяется (ADR-0052: публичный UGC под опубликованным постом — вне tenant)
Auditdelete/moderate: actor-stamp (DeletedByUserId/UpdatedByUserId) + Status + ReasonCode (модерация)

HTTP-контракт

ПолеЗначение
MethodDELETE
Route/api/v1/comments/{id}
Success status204 (No Content)
Idempotency headerне применяется (естественная идемпотентность: повтор на терминальном статусе → 204 без мутации)
Correlationсквозной trace через traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
IdrouteGuidДа!= Guid.Empty; существующий комментарий (шаг 2)
ReasonbodyCommentModerationReason?Нетenum строкой; учитывается только для модерации (актор с news.comments.moderate); для авторского удаления игнорируетсяпишется в ReasonCode audit
ActorUserIdidentity claimsGuidДаJWT аутентифицированного актора; определяет ветку record-level (шаг 2)

Enum CommentModerationReason (хранится/передаётся строкой)

ЗначениеОписание
SpamСпам/реклама.
AbuseОскорбления/травля.
OffensiveНедопустимый/противоправный контент.
OffTopicНе по теме поста.
OtherИная причина (уточняется в audit).

Unknown enum handling: неизвестное значение → отклоняется валидатором (VALIDATION_FAILED); новые причины добавляются additively.

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

Тела ответа нет — 204 No Content. Идентификаторы/статус наружу не возвращаются (клиент уже знает id; новое состояние узла клиент получает при перечитке дерева GET /api/v1/posts/{id}/comments).

Алгоритм

  1. Контекст и доступ. Выполняет автор комментария (User) либо оператор-модератор (m2m, ADR-0053). Идемпотентность обеспечивается терминальным статусом — Idempotency-header не применяется. Comment — публичный UGC (вне tenant).
  2. Проверка входа и доступа. Id != Guid.Empty; Reason (если задан) — допустимое CommentModerationReason, иначе VALIDATION_FAILED. Найти комментарий по Id; если не найден → NEWS_COMMENT_NOT_FOUND (404). Ветвление доступа (INV-CMT-004): если ActorUserId == AuthorUserIdавторская ветка; иначе актор обязан иметь permission news.comments.moderateмодераторская ветка; ни то ни другое → FORBIDDEN (403).
  3. Допустимость состояния. Переход разрешён только из Visible. Терминальные DeletedByAuthor/HiddenByModerator — идемпотентный повтор: без мутации и без повторного декремента, сразу 204 (INV-CMT-006 запрещает лишь обратный переход в Visible).
  4. Переход и декремент.
    • автор: Visible → DeletedByAuthor, DeletedAt = now, actor-stamp; дети ветки (Path/RootCommentId) не трогаются;
    • оператор: Visible → HiddenByModerator, actor-stamp, ReasonCode = Reason; DeletedAt не заполняется (это не удаление); Body при рендере не отдаётся (плейсхолдер). Денормализованный Post.CommentsCount декрементируется в той же транзакции (INV-CMT-005; ночной comment-count-reconcile страхует дрейф). Узел остаётся плейсхолдером — ветка обсуждения цела.
  5. Согласованность и результат. Переход комментария и декремент счётчика применяются атомарно, без outbox — интеграционных событий нет (удаление/скрытие не рассылается; pin.news.comment-added.v1 — только на создание). Конкурентный переход статуса → ConcurrencyConflict (409, retry с перечиткой). Тела ответа нет — 204 No Content; терминальный повтор — тот же 204.

Диаграмма

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

ТипДетали
Integration eventsnone (осознанно: удаление/скрытие не публикуется в Kafka)
External callsnone
Cache / projections / searchPost.CommentsCount декремент (денорм. для отображения/сортировки; ночной comment-count-reconcile страхует дрейф, INV-CMT-005). Узел дерева сохраняется плейсхолдером — дети целы
Notificationsnone

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Id == Guid.Empty или недопустимый Reason«Проверьте корректность запроса»no retry
NEWS_COMMENT_NOT_FOUND404комментарий не найден по Id«Комментарий не найден»no retry
FORBIDDEN403актор не автор и не имеет news.comments.moderate (INV-CMT-004)«Недостаточно прав»no retry
ConcurrencyConflict409конкурентный переход статуса (RowVersion/xmin)«Повторите попытку»retry (перечитка FindById)

Acceptance Criteria

  • Авторское удаление. Дано: Visible-комментарий автора. Когда: автор шлёт DELETE. Тогда: переход Visible → DeletedByAuthor, DeletedAt = now, Post.CommentsCount уменьшен на 1, узел остаётся плейсхолдером (дети целы), 204; тела ответа нет.
  • Модерация. Дано: Visible-комментарий, актор с news.comments.moderate, Reason = Spam. Когда: DELETE. Тогда: переход Visible → HiddenByModerator, ReasonCode = Spam, DeletedAt не заполняется, Body при рендере не отдаётся, CommentsCount уменьшен, 204.
  • Идемпотентный повтор. Дано: комментарий уже в DeletedByAuthor/HiddenByModerator. Когда: повторный DELETE. Тогда: 204 без мутации и без повторного декремента (INV-CMT-006 запрещает возврат в Visible).
  • Нет прав. Дано: актор — не автор и без news.comments.moderate. Когда: DELETE. Тогда: 403 FORBIDDEN (INV-CMT-004), состояние не меняется.
  • Не найден. Дано: Id не существует. Когда: DELETE. Тогда: 404 NEWS_COMMENT_NOT_FOUND.
  • Недопустимая причина. Дано: Reason вне enum CommentModerationReason. Когда: DELETE. Тогда: 400 VALIDATION_FAILED.
  • Конкурентный переход. Дано: два перехода одного Visible-узла одновременно. Когда: оба выполняются. Тогда: один побеждает, проигравший получает 409 ConcurrencyConflict и при retry с перечиткой видит терминальный статус → идемпотентный 204; счётчик уменьшен ровно один раз.
  • Событий нет. Дано: любое успешное удаление/скрытие. Когда: переход зафиксирован. Тогда: интеграционных событий не публикуется (в отличие от создания).

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

  • Reason (CommentModerationReason) — additive optional; новые значения enum добавляются additively, неизвестное значение → VALIDATION_FAILED. Удаление/переименование поля или смена статус-семантики — breaking (migration + rollout).
  • Logs: comment removed/hidden (CommentId, PostId, actor-kind, Reason — только для модерации); metrics: news_comment_moderation_total{action}, news_comment_delete_total; traces — сквозной trace через traceparent; dashboard news-overview (см. README, алерт «drift CommentsCount»); runbook — сверка/пересчёт CommentsCount (comment-count-reconcile).

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

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

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

  • Сущности: comment.md (дом полей, CommentStatus, INV-CMT-004/005/006), post.md (денорм. CommentsCount, гейт CommentsEnabled).
  • Permission news.comments.moderate: permission.md.
  • Сервис: News README.
  • Tenancy: ADR-0052 (публичный UGC вне tenant).