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

Entity / Aggregate: ConferenceSessionEntity — групповой звонок в чате сделки

Дом полей агрегата «Сессия конференции» контекста messenger. Групповой аудио/видео звонок нескольких участников поверх Deal-чата (chat.md). Все проекции (карточка конференции, история звонков сделки, payload доменных событий, hub-инвайты) ссылаются сюда и перечисляют только дельту. Дочерний ростер — call-participant.md; запись — call-recording.md.

Назначение

ConferenceSessionEntity — жизненный цикл группового звонка внутри чата сделки: Provisioning → Active → Ended (плюс Failed). В отличие от 1:1 (call-session.md, пара Caller/Callee), конференция имеет динамический ростер участников (call-participant.md) с пофамильным join/leave, одного модератора и авто-завершение по опустошению. Медиа (звук/видео/демонстрация экрана) идёт через внешний media-стек (SFU, ADR-0062) — агрегат хранит только факты жизненного цикла и ростер; SDP/ICE/медиа/токены доступа не персистятся (INV-CONF-7, ср. INV-CALL-4). Доступ к конференции = активное членство в её Deal-чате (record-level, INV-CHAT-4), отдельного контактного гейта нет. Запись — по политике RecordingPolicy (для звонков в сделке, как правило, включена; call-recording.md).

Классификация

ПолеЗначение
Контекстmessenger
KindAggregateRoot (дочерний CallParticipantEntity — Entity внутри агрегата, call-participant.md)
Source of truthmessenger (жизненный цикл + ростер); DealId/DealTenantId — реплики из crm через chat.md
Таблицаmessenger.conference_sessions (+ messenger.call_participants)
Tenant isolationНе применяется строково: доступ = членство в Deal-чате (INV-CHAT-4). DealTenantId — только scope арбитража/маршрутизации событий, не фильтр строк.

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKapp (v7)Идентификатор конференции.
ChatIdGuidДаFK fk_conference_sessions_chat_id → messenger.chats(id) restrict; index; часть partial ix_conf_activeappDeal-чат конференции (chat.md, Kind = Deal).
DealIdGuid?Нетindex ix_conf_deal_id; логическая ссылка crm.deals(id)реплика из chat.mdДенорм сделки для маршрутизации события conference-ended и истории звонков сделки.
DealTenantIdGuid?Нетindexреплика из chat.mdDeveloperCompanyId сделки; scope арбитража + ключ Kafka.
MediaTypeCallMediaTypeДаenum (text), varchar(8)appСтартовая модальность (аудио/видео); enum — call-session.md.
RoomMediaRoom (owned VO)Дасм. Value objectsappИдентификатор внешней медиа-комнаты (SFU, ADR-0062).
StatusConferenceStatusДаenum (text), varchar(16); partial ix_conf_active where status in ('Provisioning','Active')app / workerСм. enum и stateDiagram.
StartedByUserIdGuidДаindex; логическая ссылка identity.users(id)appИнициатор (стартовый модератор).
Titlestring?Нетvarchar(120)appНеобязательное название встречи (по умолчанию — из контекста сделки).
StartedAtDateTimeOffsetДаappМомент старта (переход → Active).
EndedAtDateTimeOffset?Нетapp / workerМомент завершения (→ Ended/Failed).
EndReasonConferenceEndReason?Нетenum (text), varchar(24)app / workerПричина завершения; обязателен при терминальном статусе.
DurationSecondsint?Нет>= 0 (check)domainВычисляется EndedAt − StartedAt при завершении.
ActiveParticipantCountintДа>= 0, default 0domainДенорм число активных участников (для авто-завершения и карточки).
PeakParticipantCountintДа>= 0, default 0domainМаксимум одновременно активных за сессию (для аналитики/события).
LastActivityAtDateTimeOffsetДаindex ix_conf_last_activityapp / webhookПоследняя presence-активность (join/leave/heartbeat); основа stale-детекции reaper-а.
CreatedAtDateTimeOffsetДаappСоздание записи (= StartedAt при старте).
UpdatedAtDateTimeOffsetДаappПоследний переход/изменение.

Enum ConferenceStatus

ЗначениеОписание
ProvisioningКомната зарезервирована, ждём подтверждения media-стека (admission-вебхук). Переходное.
ActiveИдёт конференция, ≥ 1 активный участник.
EndedШтатно завершена (опустошение / модератор / закрытие чата). Терминальный.
FailedКомната не поднялась / фатальный сбой media/записи. Терминальный.

Enum ConferenceEndReason

ЗначениеОписание
LastParticipantLeftВышел последний активный участник (штатное авто-завершение).
EmptyTimeoutReaper закрыл осиротевшую пустую комнату по истечении grace.
ModeratorEndedМодератор завершил для всех.
ChatClosedDeal-чат перешёл в Closed (сделка Completed/Cancelled, INV-CHAT-8).
TimeoutReaper закрыл «зависшую» активную сессию без активности дольше cap.
ErrorФатальный сбой media/инфраструктуры (сопровождает Failed).

Value objects

MediaRoom (owned)

Ссылка на внешнюю медиа-комнату SFU (ADR-0062). Не медиа и не PII: имя комнаты — маршрутный идентификатор, нужный для реконнекта участников и привязки записи к сессии; персистится. Токены доступа, SDP/ICE, TURN-креды — эфемерны, минтятся при входе, не хранятся (INV-CONF-7).

ПолеТипОбязательностьОграниченияКолонка
RoomNamestringДаvarchar(128); детерминирован из Id + nonce; уникален среди активных сессийroom_name
DomainstringДаvarchar(128)room_domain

Модели JSONB

JSONB-модели не используются. Медиа/сигналинг (SDP/ICE) и токены доступа в БД не хранятся (INV-CONF-7).

Связи

СвязьТипКлючПравило загрузкиDelete behavior
ChatEntityreference (родитель контекста)ChatIdгейт доступа по членству (chat.md, INV-CHAT-4)restrict
CallParticipantEntityowned-childcall_participants.conference_session_idростер конференции (call-participant.md)cascade
CallRecordingEntityreference (0..1 активная)call_recordings.conference_session_idзапись сессии (call-recording.md); связь по XOR-FK со стороны записиrestrict
Инициатор (messenger-user)replica (логическая)StartedByUserIdадресация hub-групп; без FK

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

ИнвариантУсловиеГде проверяетсяОшибка
INV-CONF-1Конференция привязана ровно к одному чату с Kind = Deal; для Direct-переписки используется 1:1 (call-session.md)старт конференцииMSG_CONFERENCE_CHAT_INVALID
INV-CONF-2Не более одной активной (Provisioning/Active) конференции на чат (partial unique ix_conf_active where status in ('Provisioning','Active'))guard стартаMSG_CONFERENCE_ALREADY_ACTIVE
INV-CONF-3Старт/присоединение = активный Participant этого чата (record-level, INV-CHAT-4); отдельного контактного гейта нетguard доступаMSG_CHAT_ACCESS_DENIED
INV-CONF-4Переходы: Provisioning → Active/Failed; Active → Ended/Failed. Ended при опустошении / модераторе / закрытии чата; reaper обеспечивает EmptyTimeout/Timeoutкомандные методы + reaperMSG_CONFERENCE_INVALID_STATE
INV-CONF-5Room.RoomName уникален среди активных сессий; токены/ICE/медиа не персистятсястарт/присоединение
INV-CONF-6Ровно один активный участник с ConferenceRole = Moderator; при выходе модератора роль передаётся (старейший активный)join/leave/end (call-participant.md)
INV-CONF-7В БД нет медиа/сигналинга/PII звонка и токенов доступа — только факты жизненного цикла и ростерсигналинг-слой (не БД)
INV-CONF-8ActiveParticipantCount/PeakParticipantCount — производные от активных строк ростера; Active с ActiveParticipantCount = 0 дольше grace → reaper закрывает (EmptyTimeout)пересчёт при join/leave + reaper

Провизия, авто-завершение и гонки. Между StartConference и admission-вебхуком media-стека конференция в Provisioning; вошедший до подтверждения участник учитывается активным ростером после admission (→ Active). Не поднявшаяся в срок комната — Provisioning → Failed (ProvisionTimeout); ConferenceEndedEvent (интеграционное conference-ended) публикуется и для Failed — downstream получает факт завершения независимо от причины. Авто-завершение: выход последнего активного участника → LastParticipantLeft; Active c ActiveParticipantCount = 0 дольше grace либо stale-сессия дольше cap → reaper (EmptyTimeout/Timeout), идемпотентно (обновляет только Provisioning/Active, INV-CONF-8). Единственная активная конференция на чат страхуется ix_conf_active: гонка двух стартов — победитель создаёт, проигравший получает MSG_CONFERENCE_ALREADY_ACTIVE (INV-CONF-2).

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
ConferenceStartedEventдоменное[*] → Provisioning/Activepush ConferenceInvite (комната + токен минтится при доставке) в hub-группу chat:{ChatId}
ConferenceParticipantJoinedEventдоменноеактивный участник вошёл (call-participant.md)push MediaParticipant(Joined) в chat:{ChatId}
ConferenceParticipantLeftEventдоменноеучастник вышелpush MediaParticipant(Left) в chat:{ChatId}
ConferenceScreenShareChangedEventдоменноестарт/стоп демонстрации экрана участникомpush ScreenShareState в chat:{ChatId}
ConferenceModeratorChangedEventдоменноепередача роли модератора (INV-CONF-6)push ConferenceSignal в chat:{ChatId}
ConferenceEndedEventдоменное + интеграционное→ Ended/Failedдоменный push ConferenceSignal(Ended); интеграционное pin.messenger.conference-ended.v1 (ключ DealId), консюмеры crm/notifications — контракт ../events/conference-ended.md

Доменные события инициируют hub-push (eventName в snake_case, только non-PII). Интеграционное ConferenceEndedEvent публикуется атомарно со сменой состояния через транзакционный outbox (persist-then-push).

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK; ix_conf_deal_id (DealId); partial unique ix_conf_active where status in ('Provisioning','Active') — guard одной активной на чат (INV-CONF-2) + выборка reaper-а; ix_conf_last_activity (LastActivityAt) — stale-детекция; FK-index по ChatId
Партиционирование / retentionnone (ADR-0056; групповые звонки редки на целевой нагрузке)
Concurrencyоптимистическая блокировка; единственная активная конференция на чат — command-guard (INV-CONF-2); reaper идемпотентен (обновляет только Provisioning/Active)
Permissions / PIIPermission messenger.conferences.use; доступ = членство в Deal-чате (INV-CHAT-4). Медиа/SDP/ICE/токены не персистятся (INV-CONF-7). Media/запись за feature-флагами CONFERENCE_CALLS_ENABLED/RECORDINGS_ENABLED
AuditUpdatedAt + фазовые метки (StartedAt/EndedAt) — иммутабельные факты; ростер участников — бизнес-аудит состава (call-participant.md)

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

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

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