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

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
KindAggregateRoot (одиночный агрегат без дочерних сущностей)
Source of truthsupport
Таблицаsupport.kb_articles
Tenant isolationTenantId = PIN CompanyId — платформенный инструмент; доступ по platform-scope, не по агентству/застройщику (ADR-0052)

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор статьи.
TenantIdGuidДаindex ix_kb_tenant_idappPIN CompanyId (единый tenant платформы).
SlugstringДаvarchar(160), не пустое, kebab-case; unique ux_kb_slug where deleted_at is nullapp (authoring)URL-суффикс/код статьи (/help/{slug}); ключ public-получения (get-kb-article).
TitlestringДаvarchar(256), не пустое; индексируется в Elasticsearch (support-kb-articles)app (authoring)Заголовок статьи (INV-KB-2).
Summarystring?Нетvarchar(1000); индексируется в Elasticsearchapp (authoring)Краткое описание/анонс (превью в списках/поиске).
Bodystring?Нет (обязательно при публикации)text; markdown/html; индексируется в Elasticsearchapp (authoring)Тело статьи; непустое требуется для перехода в Published (INV-KB-2).
CategoryKbCategoryДа.HasConversion<string>(), varchar(32)appАудитория по типу пользователя (см. enum).
AudienceKbAudienceДа.HasConversion<string>(), varchar(32); часть partial-индексаappКто может читать (см. enum, INV-KB-3).
TagsKbTags (jsonb)Даtyped jsonb, ContractVersion = 1; колонка tags; индексируется в Elasticsearchapp (authoring)Теги для фильтра/поиска; дом модели — ниже.
StatusKbStatusДа.HasConversion<string>(), varchar(16), index ix_kb_statusappСтатус жизненного цикла (см. enum).
PublishedAtDateTimeOffset?НетappМомент первой/последней публикации; null для Draft.
ViewsCountlongДаdefault 0, >= 0app (public-read)Популярность FAQ; инкремент при публичном чтении (INV-KB-4). Ранжирование по популярности — отложено.
CreatedAtDateTimeOffsetДаindex ix_kb_created_at descappСоздание статьи.
CreatedByUserIdGuid?НетappАвтор-оператор (null — SEED-заготовка).
UpdatedAtDateTimeOffsetДаappМомент последнего изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?НетappSoft-delete (скрытие статьи).
DeletedByUserIdGuid?НетappКто скрыл.
VersionlongДа>= 1appВерсия контента (инкремент при контент-правке; ревизионный 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)

KbTags (jsonb, ContractVersion = 1, колонка tags)

Типизированный список тегов статьи; участвует в фильтрации и полнотекстовом поиске (Elasticsearch).

ПолеТипОбязательностьОграниченияSince version
ItemsIReadOnlyList<string>Да (допускается пустой)элементы varchar(64), уникальные, count <= 20v1

Связи

FK-связей нет — независимый справочник. Единственная логическая связь — «по смыслу» на шаблонный ответ (ticket-template.md) при подготовке материалов оператором; ссылка не хранится (без поля и без FK).

СвязьТипКлючПравило загрузкиDelete behavior
самостоятельный агрегат (child/reference отсутствуют)

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-KB-1Slug уникален среди живых статей (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: PublicAnonymous; AuthenticatedUsers — по JWT; SupportInternal — только support.*при выдаче на public/authenticated API (фильтр по Audience/актору)SUPPORT_KB_AUDIENCE_FORBIDDEN
INV-KB-4ViewsCount инкрементится только при публичном чтении 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 → PublishedId, TenantId, Slug, Title, Category, Audience, PublishedAt
ArticleArchivedдоменноеПереход Published → ArchivedId, 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
Партиционирование / retentionnone (низкий объём справочника, ADR-0056)
Concurrencyоптимистическая блокировка (правка не должна затирать чужую); версия контента — поле Version
Permissions / PIIPublic/get — по Audience (PublicAnonymous; AuthenticatedUsers → по JWT; SupportInternalsupport.*). 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.

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

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

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