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 |
| Kind | Entity (owned-child агрегата ConferenceSession; отдельная таблица) |
| Source of truth | messenger (состав и присутствие); ФИО — реплика identity по UserId |
| Таблица | messenger.call_participants |
| Tenant isolation | Не применяется: доступ — по членству в Deal-чате конференции (INV-CHAT-4), не по TenantId. |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app (v7) | Идентификатор строки участия. |
ConferenceSessionId | Guid | Да | FK fk_call_participants_conference_id → messenger.conference_sessions(id) cascade; часть unique ux_call_participants_conf_user; часть partial ix_call_participants_active | app | Родительская конференция (conference-session.md). |
UserId | Guid | Да | часть unique ux_call_participants_conf_user (ConferenceSessionId,UserId); index ix_call_participants_user; логическая ссылка identity.users(id) | app | Пользователь-участник. |
Role | ParticipantRole | Да | .HasConversion<string>(), varchar(24) | app (снимок из participant.md) | Роль пользователя в чате на момент входа (снимок; enum — participant.md). |
ConferenceRole | ConferenceRole | Да | .HasConversion<string>(), varchar(16), default Speaker | app | Роль модерации в конференции (см. enum). |
State | CallParticipantState | Да | .HasConversion<string>(), varchar(16); часть partial ix_call_participants_active where state = 'Joined' | app / webhook | Присутствие (см. enum). |
ScreenShareState | ScreenShareState | Да | .HasConversion<string>(), varchar(8), default None | app / webhook | Демонстрация экрана участником (см. enum). Координационное состояние, не медиа-контент. |
InvitedAt | DateTimeOffset? | Нет | — | app | Момент приглашения (для инициатора/приглашённых). |
JoinedAt | DateTimeOffset? | Нет | — | app / webhook | Момент фактического входа в комнату (→ Joined). |
LeftAt | DateTimeOffset? | Нет | — | app / webhook | Момент выхода (→ Left). |
LeaveReason | ParticipantLeaveReason? | Нет | .HasConversion<string>(), varchar(24) | app / webhook | Причина выхода; обязательна при State = Left. |
DurationSeconds | int? | Нет | >= 0 (check) | domain | LeftAt − JoinedAt при выходе; иначе null. |
CreatedAt | DateTimeOffset | Да | — | app | Запись строки. |
UpdatedAt | DateTimeOffset | Да | — | 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 | загружается в составе агрегата ConferenceSession | cascade |
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-2 | JoinedAt/LeftAt — иммутабельные фазовые метки; активный участник — State = Joined | методы join/leave агрегата | MSG_CONFERENCE_INVALID_STATE |
| INV-CPART-3 | DurationSeconds = LeftAt − JoinedAt при выходе; до выхода — null | пересчёт на → Left | — |
| INV-CPART-4 | ScreenShareState отражает координационное состояние демонстрации (не медиа-контент); синхронизируется из 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 | доменное | новый/реактивированный Joined | push MediaParticipant(Joined) в hub-группу chat:{ChatId}. Поля: ConferenceSessionId, UserId, Role |
ConferenceParticipantDeclinedEvent | доменное | Invited → Declined | push MediaParticipant(Left) инициатору приглашения / в chat:{ChatId}. Поля: ConferenceSessionId, UserId |
ConferenceParticipantLeftEvent | доменное | State = Left (сам вышел / удалён модератором / дисконнект) | push MediaParticipant(Left). Поля: ConferenceSessionId, UserId, LeaveReason |
ConferenceScreenShareChangedEvent | доменное | смена ScreenShareState | push 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 и авторитетный ростер |
| Партиционирование / retention | none (ADR-0056; десятки участников на конференцию); строки не удаляются (soft-leave, история) |
| Concurrency | в границах агрегата ConferenceSession (оптимистическая блокировка родителя); отдельной блокировки у участника нет |
| Permissions / PII | Доступ к ростеру = членство в Deal-чате конференции (INV-CHAT-4). Строка PII не хранит (UserId-ссылка, не ФИО); ФИО резолвятся из реплики identity. Живой mute/камера — вне БД |
| Audit | soft-leave (LeftAt/LeaveReason) + фазовые метки — бизнес-аудит участия; отдельной Audit-сущности нет |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: ConferenceSessionEntity — групповой звонок в чате сделки
- API (12): GET /api/messenger/conferences/{conferenceId} — Карточка конференции, POST /api/messenger/chats/{chatId}/conferences — Начать конференцию в чате сделки, POST /api/messenger/conferences/{conferenceId}/decline — Отклонить приглашение в конференцию, POST /api/messenger/conferences/{conferenceId}/end — Завершить конференцию для всех, POST /api/messenger/conferences/{conferenceId}/invite — Пригласить участников в конференцию, POST /api/messenger/conferences/{conferenceId}/join — Присоединиться к конференции, POST /api/messenger/conferences/{conferenceId}/leave — Выйти из конференции, POST /api/messenger/conferences/{conferenceId}/media-token — Обновить токен входа в конференцию, POST /api/messenger/conferences/{conferenceId}/participants/{userId}/remove — Удалить участника из конференции, POST /internal/jitsi/events — Presence-вебхук media-стека (авторитет ростера), POST/DELETE /internal/jitsi/reservations — Резервирование медиа-комнаты (prosody admission), SignalR Hub /hubs/messenger — MessengerHub (realtime-контракт)
- Use Cases (1): UC-MSG-006. Групповой звонок-конференция в чате сделки
- События (1): Event Contract: pin.messenger.conference-ended (ConferenceEndedEvent)
- Фоновые задачи (2): Background Job: ConferenceRoomReaperJob, Background Job: RealtimeDispatchJob
- adr (1): ADR-0062: Медиа-стек звонков и запись на Jitsi (JVB SFU + coturn + jibri → file storage; JWT из messenger; нативный lib-jitsi-meet)
- calls (7): Жизненный цикл сессии звонка — 1:1 и конференция (messenger), Медиа-звонки и запись на Jitsi — интеграционный гид (messenger), Медиа-звонки на Jitsi: план поставки (playbook аналитик → разработчик), Backend-реализация медиа-звонков: спека → код (messenger), Jitsi: именование комнат, admission и presence, Jitsi: нативный клиент lib-jitsi-meet на Next.js 16, Jitsi: k8s-топология и взаимодействие с бэкендом
- registries (1): Реестр сущностей PIN
Связанные документы
- Родитель-агрегат (дом конференции, INV-CONF-6): conference-session.md; 1:1-аналог: call-session.md.
- Орг-роли участников: participant.md; реплика имён: identity/events/user-upserted.md.
- Presence-вебхук (авторитет ростера): ../api/internal/jitsi-events.md.
- Решение: ADR-0055, ADR-0062.