GET /api/public/support/kb/articles/{slug} — Статья базы знаний
Публичное получение одной статьи справки/FAQ по Slug для витрины помощи ПИН (/help/{slug}). Отдаёт
только опубликованную статью, доступную актору по аудитории; иначе — 404 (существование скрытого/чернового
контента не раскрывается). Проекция статьи KnowledgeBaseArticleEntity — дом полей в
knowledge-base-article.md; здесь описана только public-дельта.
Списочная/поисковая выдача — search-kb-articles.
Назначение
Дать гостю или авторизованному пользователю прочитать конкретную статью базы знаний по человекочитаемому
Slug (матрица 4.7 «база знаний для пользователей», 7.1/7.2). Побочно — приблизительно учесть популярность
статьи (ViewsCount, INV-KB-4) без влияния на ответ.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | support |
| API area | public |
| Feature group | knowledge-base |
| Статус | draft |
| Документ | docs/06-services/support/api/public/get-kb-article.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | Anonymous (гость) или User (любой авторизованный по JWT) |
| Auth policy / Permissions | none — публичное чтение; доступ решается audience-gate (шаг 4), не permission |
| Scope (RBAC) | не применяется (актор — вне platform-scope; support.kb.view нужен только для support-области) |
| Record-level | audience-gate по Audience статьи vs тип актора (INV-KB-3) |
| Tenant isolation | TenantId = PIN CompanyId — single-tenant platform-tool (ADR-0052); резолвится сервером, не приходит из запроса |
| Audit | read (не sensitive); инкремент ViewsCount — не аудит-событие |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/public/support/kb/articles/{slug} |
| Success status | 200 |
| Idempotency header | не применяется (чтение; ViewsCount — best-effort, приблизительный) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
Slug | route | string | Да | kebab-case ^[a-z0-9]+(?:-[a-z0-9]+)*$, 1..160 (ICodeEntity) | — |
ActorType | identity claims | ActorType (enum строкой) | Нет | Anonymous по умолчанию, User при валидном JWT | вход для audience-gate |
TenantId | server (platform) | Guid | Да | = PIN CompanyId; не из запроса (ADR-0052) | tenant фиксирован |
Модель ответа KbArticleModel
Проекция KnowledgeBaseArticleEntity (дом полей — knowledge-base-article.md);
здесь только public-дельта. Возвращается один объект (не список).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Title | string | Да | entity.Title | — | Заголовок статьи. |
Summary | string? | Нет | entity.Summary | — | Краткое описание/анонс. |
Body | string | Да | entity.Body | — | Тело (markdown/html); непустое гарантировано публикацией (INV-KB-2). |
Category | KbCategory | Да | entity.Category | — | Категория аудитории; сериализация строкой (enum home — domain). |
Tags | KbTags | Да | entity.Tags | — | Теги; коллекция живёт в именованном контейнере KbTags { Items } (jsonb, ContractVersion = 1). |
PublishedAt | DateTimeOffset | Да | entity.PublishedAt | — | Момент публикации; non-null, т.к. отдаётся только Published. |
Id, Slug, Status, Audience, ViewsCount и аудит-поля наружу не возвращаются (внутренние/служебные).
Алгоритм
-
Контекст и доступ. Читателем может быть гость (
Anonymous) или любой авторизованный пользователь (User, JWT опционален). support — единый платформенный tenant (TenantId = PIN CompanyId, резолвится сервером).Slug— из маршрута. Permission не требуется; полнота доступа решается audience-gate (шаг 3). -
Проверка входа.
Slugне пустой, kebab-case, длина1..160; нарушение →VALIDATION_FAILED. -
Поиск статьи и audience-gate. Статья ищется по
Slugсреди живых (удалённые исключены). Любой из отказов ниже схлопывается вSUPPORT_KB_NOT_FOUND(404) — существование скрытого контента не раскрывается:- статья не найдена → 404;
- состояние:
Status == Published, иначе → 404; - аудитория (INV-KB-3):
Audience == Public→ любой актор;Audience == AuthenticatedUsers→ только авторизованныйUser;Audience == SupportInternal→ на public не отдаётся никогда → 404. Отдельный кодSUPPORT_KB_AUDIENCE_FORBIDDEN(403) наружу не выводится — чтобы не раскрывать наличие статьи.
При успехе возвращаемые поля берутся напрямую из статьи:
Title,Summary,Body,Category(enum строкой),Tags(KbTags),PublishedAt. -
Best-effort учёт популярности. После успешного gate счётчик
ViewsCountинкрементируется приблизительно (INV-KB-4): побочно, вне критического пути ответа, без блокировок и доменных событий; ошибка инкремента логируется (warn) и глотается, на ответ не влияет. Идемпотентность не требуется — счётчик приблизительный. -
Результат. Возвращается
KbArticleModel→ HTTP200+ body. Любой отказ (не найдена / неPublished/ аудитория не совпала) →SUPPORT_KB_NOT_FOUND→404.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (read; KB не публикует Kafka-событий) |
| External calls | none |
| Cache / projections / search | best-effort инкремент support.kb_articles.views_count (приблизительный, INV-KB-4); read-cache не применяется в первой версии (низкий объём, ADR-0056) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | пустой/некорректный Slug (формат) | «Проверьте адрес статьи» | no retry |
SUPPORT_KB_NOT_FOUND | 404 | статья не найдена, либо Status != Published, либо аудитория не совпала (схлопнуто) | «Статья не найдена» | no retry |
Acceptance criteria
- AC-1 (публичная статья). Given статья
Status = Published,Audience = Public; WhenGET .../kb/articles/{slug}гостем (Anonymous); Then200сTitle/Summary/Body/Category/Tags/PublishedAt;ViewsCountинкрементируется best-effort (вне ответа). - AC-2 (только авторизованным). Given статья
Audience = AuthenticatedUsers; When запрос гостем →SUPPORT_KB_NOT_FOUND(404); When тот же запрос авторизованнымUser→200(INV-KB-3). - AC-3 (внутренняя статья скрыта). Given статья
Audience = SupportInternal; When любой public-запрос; ThenSUPPORT_KB_NOT_FOUND(404) — на public не отдаётся никогда, факт наличия не раскрывается (кодSUPPORT_KB_AUDIENCE_FORBIDDENнаружу не выводится). - AC-4 (не опубликована). Given статья
Status ∈ {Draft, Archived}или soft-deleted; When запрос поSlug; Then404(схлопнуто вSUPPORT_KB_NOT_FOUND). - AC-5 (нет статьи). Given несуществующий
Slug; When запрос; Then404. - AC-6 (формат slug). Given
Slugпустой/не kebab-case/>160; When запрос; ThenVALIDATION_FAILED(400). - AC-7 (учёт просмотра не влияет на ответ). Given статья доступна, но инкремент
ViewsCountупал; When запрос; Then ответ всё равно200с телом; ошибка инкремента логируется (warn) и глотается (INV-KB-4, best-effort).
Совместимость и наблюдаемость
- Добавление optional-полей в
KbArticleModel— additive (не breaking); удаление/переименование/смена типа — breaking (migration + rollout).Categoryсериализуется строкой — клиент обязан толерантно обрабатывать неизвестные значения enum (forward-compat). - Logs:
kb-article served/kb-article not-found(Slug,result; причину gate не раскрывать в 404-теле); metrics:support_kb_article_reads_total{result}, latency p95 < 300 мс; traces — OTel; dashboard Grafanapin-support; runbook —docs/06-services/support/runbooks/(будущий путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: KnowledgeBaseArticleEntity
- API (6): GET /api/public/support/kb/articles — Поиск базы знаний/FAQ, GET /api/support/kb/articles — Список статей базы знаний (вкл. черновики), POST /api/support/kb/articles — Создать статью базы знаний, POST /api/support/kb/articles/{id}/archive — Снять статью базы знаний с учёта, POST /api/support/kb/articles/{id}/publish — Опубликовать/снять статью базы знаний, PUT /api/support/kb/articles/{id} — Обновить статью базы знаний
- Поиск (1): KbArticleSearchDocument
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность/проекция: knowledge-base-article.md (
KbArticleModel,KbCategory,KbAudience,KbStatus,KbTags, INV-KB-3/INV-KB-4); соседний endpoint — search-kb-articles; README контекста. - Tenancy — ADR-0052; FTS/масштаб — ADR-0056.