Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
# ADR-003. Модель приглашённых участников конференции (conference_invitees)
Статус: принято.
## Контекст
Продукту нужен состав приглашённых участников конференции: зарегистрированные
пользователи (user_id) и внешние по произвольному email. Организатор обязан
всегда быть в составе и быть неудаляемым. Уже существует таблица
`conference_participants` — это ФАКТИЧЕСКИЕ участники сеанса (кто реально был,
окна присутствия, единица атрибуции фраз, ADR-002); смешивать сущности нельзя.
## Решение
1. Новая таблица `conference_invitees` — приглашённые НА КОНФЕРЕНЦИЮ
(не на сеанс):
- `id UUID PK`, `conference_id FK conferences ON DELETE CASCADE NOT NULL`;
- `user_id FK users ON DELETE CASCADE NULL` — зарегистрированный;
- `email VARCHAR(255) NULL` — внешний (хранится в lower-case);
- `CHECK ((user_id IS NOT NULL)::int + (email IS NOT NULL)::int = 1)`
ровно одна identity (тот же приём, что в `conference_participants`);
- частичные UNIQUE: `(conference_id, user_id) WHERE user_id IS NOT NULL`
и `(conference_id, email) WHERE email IS NOT NULL` — без дублей;
- `created_at`.
2. Организатор в таблице НЕ хранится: он выводится из `conferences.owner_id`
и всегда добавляется в состав на уровне API/рассылки. Инвариант
«организатор всегда в составе и неудаляем» обеспечен конструктивно —
удалить его из состава невозможно в принципе, рассинхронизация при смене
владельца исключена. Попытка добавить владельца в invitees (по user_id или
его email) молча дедуплицируется на записи.
3. Состав задаётся списком целиком (PUT-семантика поля `participants` в
create/update конференции): backend вычисляет diff, отсутствие поля —
«не менять». Права на изменение состава = права на изменение конференции.
4. Связь с фактическими участниками сеанса — аналитическая, по join без FK:
зарегистрированный — `conference_participants.user_id = invitees.user_id`;
внешний — `lower(guest_access.email) = invitees.email` (если приглашённый
вошёл гостем и указал тот же email). FK не вводим: гость может войти
с другим email или не войти вовсе — жёсткая связь ложна по природе данных.
5. Рассылка приглашений (.ics METHOD:REQUEST): получатели =
организатор + invitees (email пользователя или внешний email); для
закреплённых по-прежнему добавляются участники прошлых сеансов
(`workers/tasks/invitations.py`, дедуп по lower(email)).
6. **Видимость приглашённого в списках.** Приглашённый видит конференцию в
`GET /conferences/my` и `GET /conferences/calendar` наравне с владельцем —
строка попадает в выборку, если `owner_id == user.id` ИЛИ существует
`conference_invitees` этой конференции с `user_id == user.id` ИЛИ с
`lower(email) == lower(email пользователя)` (внешнее приглашение на адрес,
под которым человек впоследствии зарегистрировался). Критерии показа
(закреплённая — безусловно; разовая — `status=scheduled` и `scheduled_at`
в будущем) не меняются, только круг «чей» конференция. `GET /conferences/{id}`
аналогично открыт приглашённому (иначе ховер-карточка/детальная страница
получали бы 403); `is_owner` в ответе для приглашённого — `false`,
`organizer_name` — имя фактического владельца. Права на PATCH/DELETE это
расширение НЕ затрагивает — по-прежнему только владелец/администратор.
## Последствия
- (+) Чистое разделение «приглашён» / «фактически был»; пайплайн атрибуции
фраз (ADR-002) не затронут.
- (+) Инвариант организатора не требует триггеров и проверок целостности.
- () Внешний приглашённый не связывается с гостевым входом надёжно (только
эвристика по email) — принято как ограничение модели.
- () Списки состава в ответах API требуют дозагрузки (`selectinload`) —
следить за N+1 в `/my` и `/calendar`; показ приглашённому (п. 6) добавляет
туда же `EXISTS`-подзапрос по `conference_invitees` и точечный запрос имени
реального владельца на каждую НЕ свою строку списка — список короткий
(закреплённые + предстоящие), нагрузка признана приемлемой.
- Календарь и «Мои конференции» — выборка «владелец ИЛИ приглашённый» (п. 6);
до 2026-07-20 показывались только конференции владельца — приглашённый
видел состав лишь через уведомление/.ics, не через списки приложения.