Entity / Aggregate: KnowledgeBaseArticleEntity
Дом полей агрегата «Статья базы знаний / FAQ» контекста support (greenfield, матрица 4.7 «база знаний
для пользователей», 7.1 «обучающие материалы», 7.2 «создание инструкций»). Все проекции (public-карточка,
результаты полнотекстового поиска, payload доменных событий) ссылаются сюда и описывают только дельту, а не
переописывают поля.
Назначение
KnowledgeBaseArticleEntity — публикуемая статья справки/FAQ ПИН: инструкция или обучающий материал,
доступный пользователям по аудитории (гость / авторизованный / служба заботы) и находимый полнотекстовым
поиском (Elasticsearch-проекция support-kb-articles — ../search/kb.md,
ADR-0059; source of truth остаётся в
PostgreSQL). Aggregate root — независимый справочник: FK на другие сущности нет,
жизненный цикл (черновик → публикация → архив) и контент ведут операторы/супервизоры службы заботы.
Ревизионная история (полный snapshot по образцу news) в первой версии отложена: версия контента
ведётся полем Version + аудит (README «Известные ограничения»).
Классификация
| Поле | Значение |
|---|
| Контекст | support |
| Kind | AggregateRoot (одиночный агрегат без дочерних сущностей) |
| Source of truth | support |
| Таблица | support.kb_articles |
| Tenant isolation | TenantId = PIN CompanyId — платформенный инструмент; доступ по platform-scope, не по агентству/застройщику (ADR-0052) |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|
Id | Guid | Да | PK | app | Идентификатор статьи. |
TenantId | Guid | Да | index ix_kb_tenant_id | app | PIN CompanyId (единый tenant платформы). |
Slug | string | Да | varchar(160), не пустое, kebab-case; unique ux_kb_slug where deleted_at is null | app (authoring) | URL-суффикс/код статьи (/help/{slug}); ключ public-получения (get-kb-article). |
Title | string | Да | varchar(256), не пустое; индексируется в Elasticsearch (support-kb-articles) | app (authoring) | Заголовок статьи (INV-KB-2). |
Summary | string? | Нет | varchar(1000); индексируется в Elasticsearch | app (authoring) | Краткое описание/анонс (превью в списках/поиске). |
Body | string? | Нет (обязательно при публикации) | text; markdown/html; индексируется в Elasticsearch | app (authoring) | Тело статьи; непустое требуется для перехода в Published (INV-KB-2). |
Category | KbCategory | Да | .HasConversion<string>(), varchar(32) | app | Аудитория по типу пользователя (см. enum). |
Audience | KbAudience | Да | .HasConversion<string>(), varchar(32); часть partial-индекса | app | Кто может читать (см. enum, INV-KB-3). |
Tags | KbTags (jsonb) | Да | typed jsonb, ContractVersion = 1; колонка tags; индексируется в Elasticsearch | app (authoring) | Теги для фильтра/поиска; дом модели — ниже. |
Status | KbStatus | Да | .HasConversion<string>(), varchar(16), index ix_kb_status | app | Статус жизненного цикла (см. enum). |
PublishedAt | DateTimeOffset? | Нет | — | app | Момент первой/последней публикации; null для Draft. |
ViewsCount | long | Да | default 0, >= 0 | app (public-read) | Популярность FAQ; инкремент при публичном чтении (INV-KB-4). Ранжирование по популярности — отложено. |
CreatedAt | DateTimeOffset | Да | index ix_kb_created_at desc | app | Создание статьи. |
CreatedByUserId | Guid? | Нет | — | app | Автор-оператор (null — SEED-заготовка). |
UpdatedAt | DateTimeOffset | Да | — | app | Момент последнего изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | — | app | Soft-delete (скрытие статьи). |
DeletedByUserId | Guid? | Нет | — | app | Кто скрыл. |
Version | long | Да | >= 1 | app | Версия контента (инкремент при контент-правке; ревизионный snapshot отложен). |
Enum KbCategory
| Значение | Описание |
|---|
Investor | Материалы для инвесторов. |
Agent | Материалы для агентов. |
Developer | Материалы для застройщиков. |
General | Общие статьи (для всех типов пользователей). |
SupportInternal | Внутренние инструкции службы заботы (7.x). |
Enum KbAudience
| Значение | Описание |
|---|
Public | Доступно гостю (Anonymous) и всем авторизованным. |
AuthenticatedUsers | Доступно только авторизованным пользователям (по JWT). |
SupportInternal | Доступно только службе заботы (permission support.*). |
Enum KbStatus
| Значение | Описание |
|---|
Draft | Черновик; вне public/authenticated выдачи. |
Published | Опубликована; отдаётся по Audience, попадает в поиск (Elasticsearch-проекция). |
Archived | Снята с публикации (без удаления); повторная публикация возможна. |
Value objects
VO/owned-типы не используются (заголовок и тело — скалярные поля; теги — jsonb-модель ниже).
Модели JSONB (IDocument, versioned)
Типизированный список тегов статьи; участвует в фильтрации и полнотекстовом поиске (Elasticsearch).
| Поле | Тип | Обязательность | Ограничения | Since version |
|---|
Items | IReadOnlyList<string> | Да (допускается пустой) | элементы varchar(64), уникальные, count <= 20 | v1 |
Связи
FK-связей нет — независимый справочник. Единственная логическая связь — «по смыслу» на шаблонный ответ
(ticket-template.md) при подготовке материалов оператором; ссылка не хранится (без
поля и без FK).
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|
| — | — | — | самостоятельный агрегат (child/reference отсутствуют) | — |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|
| INV-KB-1 | Slug уникален среди живых статей (deleted_at is null) | уникальный индекс ux_kb_slug + проверка при создании/обновлении статьи | SUPPORT_KB_SLUG_DUPLICATE |
| INV-KB-2 | Публикация (Draft/Archived → Published) требует непустых Title и Body | при публикации статьи | SUPPORT_KB_PUBLISH_REQUIRES_CONTENT |
| INV-KB-3 | На public/authenticated API отдаются только Status = Published с подходящей Audience: Public — Anonymous; AuthenticatedUsers — по JWT; SupportInternal — только support.* | при выдаче на public/authenticated API (фильтр по Audience/актору) | SUPPORT_KB_AUDIENCE_FORBIDDEN |
| INV-KB-4 | ViewsCount инкрементится только при публичном чтении Published; счётчик приблизительный (идемпотентность не требуется) | при публичном чтении статьи | — |
Предусловия и постусловия переходов
| Переход | Инициатор / условие | Предусловие | Постусловие (гарантируется) |
|---|
Create ([*] → Draft) | оператор/супервизор (support.kb.manage) | Slug уникален среди живых статей (INV-KB-1, иначе SUPPORT_KB_SLUG_DUPLICATE); Title непуст | статья Draft, Version = 1, вне public/authenticated выдачи и вне поискового индекса |
Publish (Draft/Archived → Published) | оператор/супервизор | Title и Body непусты (INV-KB-2, иначе SUPPORT_KB_PUBLISH_REQUIRES_CONTENT) | PublishedAt проставлен; статья отдаётся по Audience (INV-KB-3); документ появляется в Elasticsearch-проекции support-kb-articles (окно свежести — секунды) |
Archive (Published → Archived) | оператор/супервизор | статья Published | статья снята с public/authenticated выдачи; документ удаляется из поискового индекса (fail-closed); source-of-truth сохранён (не удаление) |
SoftDelete (Archived → [*], DeletedAt) | оператор/супервизор | статья не на public-выдаче | Slug освобождается для повторного использования; документ отсутствует в индексе |
Согласованность поиска и правок контента
- Публикация → находимость. Переход в
Published и наполнение поисковой проекции разнесены во времени:
source of truth (PostgreSQL) меняется сразу, документ в support-kb-articles появляется по доменному
событию ArticlePublished (окно eventual-consistency — до ~1 мин, ../search/kb.md). В
промежутке статья уже доступна по прямому get (/help/{slug}), но ещё может не находиться поиском —
для пользователя это отставание индекса, а не потеря статьи.
- Смена
Audience у Published-статьи. Сужение аудитории (Public → AuthenticatedUsers/SupportInternal)
переиндексирует документ с новым security-trimming; до применения правки поиск может краткий промежуток
отдавать прежнюю аудиторию — фактический доступ на детальном чтении всегда проверяется по Audience
актуальной записи (INV-KB-3, иначе SUPPORT_KB_AUDIENCE_FORBIDDEN), поэтому утечки контента нет.
- Конкурентная контент-правка. Одновременные правки тела/полей разводятся оптимистичной блокировкой
(правка не должна затирать чужую);
Version инкрементится у победителя, проигравший повторяет на свежей
версии. ViewsCount — приблизительный счётчик (INV-KB-4): инкремент при публичном чтении идемпотентности
не требует, потерянные под конкуренцией единицы допустимы.
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|
ArticlePublished | доменное | Переход Draft/Archived → Published | Id, TenantId, Slug, Title, Category, Audience, PublishedAt |
ArticleArchived | доменное | Переход Published → Archived | Id, TenantId, Slug, DeletedByUserId? |
Интеграционных Kafka-событий агрегат не публикует: база знаний — внутренний справочник, внешние контексты на
изменения статей не реагируют. Доменные события обрабатываются внутренними подписчиками при смене статуса.
Индексы, хранение, безопасность
| Аспект | Значение |
|---|
| Индексы | PK; ux_kb_slug unique partial where deleted_at is null; ix_kb_tenant_id btree; ix_kb_status btree; ix_kb_category_audience btree (category, audience) partial where status = 'Published'; ix_kb_created_at btree desc |
| Поиск | публичный полнотекстовый поиск по заголовку/анонсу/телу/тегам — Elasticsearch-проекция support-kb-articles (../search/kb.md, ADR-0059), а не PostgreSQL-FTS; индексируется только Published-срез, наполнение — доменными событиями статьи + rebuild |
| Партиционирование / retention | none (низкий объём справочника, ADR-0056) |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую); версия контента — поле Version |
| Permissions / PII | Public/get — по Audience (Public → Anonymous; AuthenticatedUsers → по JWT; SupportInternal → support.*). support.kb.view (наблюдатель — все статьи, включая Draft); support.kb.manage (оператор/супервизор — CRUD/publish/archive), platform-scope (ADR-0052). PII — нет (контент справочный). |
| Audit | отметки создания/изменения/удаления (кто и когда), soft-delete; отдельной ревизионной сущности нет (snapshot отложен) |
| SEED | Опционально — стартовые FAQ-заготовки (Category = General) в Pin.Support.Migrator. |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы