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

Entity: CallParticipantEntity (owned-child ConferenceSessionEntity)

Дом полей сущности «Участник конференции» контекста messenger — пофамильный ростер группового звонка (conference-session.md): вход/выход, роль модерации, состояние демонстрации экрана. Все проекции (карточка конференции, история участия, hub-события участников) ссылаются сюда и описывают только дельту. Дом самой конференции — conference-session.md.

Назначение

CallParticipantEntity — строка «пользователь ↔ конференция» в таблице messenger.call_participants, owned-child агрегата ConferenceSessionEntity (жизненный цикл, транзакционная граница и concurrency — в границах агрегата конференции). Держит: роль в чате-снимок (Role), роль модерации (ConferenceRole), состояние присутствия (State), состояние демонстрации экрана (ScreenShareState), фазовые метки Invited/Joined/LeftAt и причину выхода. Ростер — авторитетный источник активного состава: формируется командами join/leave и сверяется presence-вебхуками media-стека (server-side, надёжнее клиентских событий). Активный участник — State = Joined. Только для групповых конференций; в 1:1 (call-session.md) участники — фиксированные Caller/Callee, отдельный ростер не заводится. Живое состояние микрофона/камеры принадлежит media-стеку и не персистится.

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

ПолеЗначение
Контекстmessenger
KindEntity (owned-child агрегата ConferenceSession; отдельная таблица)
Source of truthmessenger (состав и присутствие); ФИО — реплика identity по UserId
Таблицаmessenger.call_participants
Tenant isolationНе применяется: доступ — по членству в Deal-чате конференции (INV-CHAT-4), не по TenantId.

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKapp (v7)Идентификатор строки участия.
ConferenceSessionIdGuidДаFK fk_call_participants_conference_id → messenger.conference_sessions(id) cascade; часть unique ux_call_participants_conf_user; часть partial ix_call_participants_activeappРодительская конференция (conference-session.md).
UserIdGuidДачасть unique ux_call_participants_conf_user (ConferenceSessionId,UserId); index ix_call_participants_user; логическая ссылка identity.users(id)appПользователь-участник.
RoleParticipantRoleДа.HasConversion<string>(), varchar(24)app (снимок из participant.md)Роль пользователя в чате на момент входа (снимок; enum — participant.md).
ConferenceRoleConferenceRoleДа.HasConversion<string>(), varchar(16), default SpeakerappРоль модерации в конференции (см. enum).
StateCallParticipantStateДа.HasConversion<string>(), varchar(16); часть partial ix_call_participants_active where state = 'Joined'app / webhookПрисутствие (см. enum).
ScreenShareStateScreenShareStateДа.HasConversion<string>(), varchar(8), default Noneapp / webhookДемонстрация экрана участником (см. enum). Координационное состояние, не медиа-контент.
InvitedAtDateTimeOffset?НетappМомент приглашения (для инициатора/приглашённых).
JoinedAtDateTimeOffset?Нетapp / webhookМомент фактического входа в комнату (→ Joined).
LeftAtDateTimeOffset?Нетapp / webhookМомент выхода (→ Left).
LeaveReasonParticipantLeaveReason?Нет.HasConversion<string>(), varchar(24)app / webhookПричина выхода; обязательна при State = Left.
DurationSecondsint?Нет>= 0 (check)domainLeftAt − JoinedAt при выходе; иначе null.
CreatedAtDateTimeOffsetДаappЗапись строки.
UpdatedAtDateTimeOffsetДаappПоследнее изменение.

Enum CallParticipantState

ЗначениеОписание
InvitedПриглашён (получил инвайт), в комнату ещё не вошёл.
JoinedАктивен в комнате. Реконнект реактивирует (Left → Joined).
LeftВышел/удалён/отключился (soft — строка сохраняется в ростере).
DeclinedОтклонил приглашение.

Enum ConferenceRole

Роль модерации в конференции, ортогональна орг-роли ParticipantRole.

ЗначениеОписание
ModeratorМожет завершить конференцию, удалять/выключать других. Инициатор — модератор по умолчанию; при выходе роль передаётся (INV-CONF-6).
SpeakerОбычный участник.

Enum ScreenShareState

ЗначениеОписание
NoneНе демонстрирует экран.
SharingДемонстрирует экран (активный desktop-трек).

Enum ParticipantLeaveReason

ЗначениеОписание
SelfLeftВышел сам.
RemovedByModeratorУдалён модератором.
DisconnectedРазрыв сети без реконнекта в grace-окно.
ConferenceEndedКонференция завершена под участником.

Value objects

Value objects не используются (все поля — скаляры / enum-строка).

Модели JSONB

JSONB-модели не используются. Живое состояние микрофона/камеры/активного спикера — эфемерно, принадлежит media-стеку и в БД не хранится (ср. INV-CONF-7).

Связи

СвязьТипКлючПравило загрузкиDelete behavior
ConferenceSessionEntity (родитель)owned (в агрегате)ConferenceSessionIdзагружается в составе агрегата ConferenceSessioncascade
UserEntity (identity)replica display-полейUserId (логическая)ФИО по pin.identity.users.v1

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

ИнвариантУсловиеГде проверяетсяОшибка
INV-CPART-1Уникальность активного (ConferenceSessionId, UserId): повторный вход реактивирует строку (State = Joined, новый JoinedAt), а не создаёт дубльunique ux_call_participants_conf_user + метод агрегатаMSG_CONFERENCE_PARTICIPANT_DUPLICATE
INV-CPART-2JoinedAt/LeftAt — иммутабельные фазовые метки; активный участник — State = Joinedметоды join/leave агрегатаMSG_CONFERENCE_INVALID_STATE
INV-CPART-3DurationSeconds = LeftAt − JoinedAt при выходе; до выхода — nullпересчёт на → Left
INV-CPART-4ScreenShareState отражает координационное состояние демонстрации (не медиа-контент); синхронизируется из presence-сигналов best-effortобновление из join/screen-share-сигнала
INV-CPART-5Ростер — авторитет активного состава: presence-вебхук media-стека сверяет команды join/leave; расхождение разрешается в пользу серверного событияconsumer presence-вебхука (../api/internal/jitsi-events.md)

Подключение и отключение участников. Ростер меняют четыре команды (плюс серверный presence-вебхук как авторитет, INV-CPART-5):

  • Присоединение (../api/conferences/join-conference.md) — участник Deal-чата сам входит: [*]/Invited/Left → Joined (идемпотентно, реактивация существующей строки).
  • Адресное приглашение (../api/conferences/invite-participant.md) — активный участник зовёт конкретных членов чата: строки Invited + адресный ConferenceInvite в user:{id}; приглашённый затем join-ится. (Стартовый broadcast-ConferenceInvite в chat:{ChatId} — при StartConference.)
  • Отклонение (../api/conferences/decline-conference.md) — приглашённый отказывается: Invited → Declined.
  • Удаление модератором (../api/conferences/remove-participant.md) — модератор исключает участника: Joined → Left (RemovedByModerator) + eject из комнаты media-стеком.

Выход по своей воле — SelfLeft; разрыв сети без реконнекта в grace — Disconnected; завершение всей конференции — ConferenceEnded. Полный разбор — ../calls/session-lifecycle.md.

Реконнект и авторитет присутствия. Разрыв сети переводит участника в Left c LeaveReason = Disconnected только после истечения grace-окна без реконнекта; реконнект в пределах окна реактивирует ту же строку (Left → Joined, новый JoinedAt), дубля не создаёт (INV-CPART-1). Один пользователь с двух устройств — одна строка ростера (ux_call_participants_conf_user): второй вход реактивирует существующую. Расхождение presence-вебхука media-стека и локальных команд join/leave разрешается в пользу серверного вебхука (INV-CPART-5) — ростер сходится к состоянию media-стека.

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

Все события — доменные, поднимаются агрегатом ConferenceSession при изменениях ростера и обрабатываются в той же транзакции; persist-then-push (ADR-0055): сначала фиксация, затем push в SignalR-hub. Собственных Kafka-событий участник не публикует.

СобытиеТипКогдаПобочный эффект / snapshot
ConferenceParticipantInvitedEventдоменноесоздана строка Invited (адресный invite/ring)push ConferenceInvite в user:{UserId} (комната + токен минтится при доставке). Поля: ConferenceSessionId, UserId
ConferenceParticipantJoinedEventдоменноеновый/реактивированный Joinedpush MediaParticipant(Joined) в hub-группу chat:{ChatId}. Поля: ConferenceSessionId, UserId, Role
ConferenceParticipantDeclinedEventдоменноеInvited → Declinedpush MediaParticipant(Left) инициатору приглашения / в chat:{ChatId}. Поля: ConferenceSessionId, UserId
ConferenceParticipantLeftEventдоменноеState = Left (сам вышел / удалён модератором / дисконнект)push MediaParticipant(Left). Поля: ConferenceSessionId, UserId, LeaveReason
ConferenceScreenShareChangedEventдоменноесмена ScreenShareStatepush ScreenShareState. Поля: ConferenceSessionId, UserId, ScreenShareState

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

АспектЗначение
ИндексыPK; ux_call_participants_conf_user unique (conference_session_id,user_id); ix_call_participants_user (user_id) — история участия; partial ix_call_participants_active (conference_session_id) where state = 'Joined' — пересчёт ActiveParticipantCount и авторитетный ростер
Партиционирование / retentionnone (ADR-0056; десятки участников на конференцию); строки не удаляются (soft-leave, история)
Concurrencyв границах агрегата ConferenceSession (оптимистическая блокировка родителя); отдельной блокировки у участника нет
Permissions / PIIДоступ к ростеру = членство в Deal-чате конференции (INV-CHAT-4). Строка PII не хранит (UserId-ссылка, не ФИО); ФИО резолвятся из реплики identity. Живой mute/камера — вне БД
Auditsoft-leave (LeftAt/LeaveReason) + фазовые метки — бизнес-аудит участия; отдельной Audit-сущности нет

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

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

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