feat(dm): хранилище и миграция звонков в беседах (Фаза 7)

Звонок в личной и групповой беседе живёт в своей таблице: в voice_states
guild_id обязателен, а у беседы сервера нет. Миграция 00026 добавляет
dm_calls (статус, media, длительность, причина завершения) и
dm_call_participants (состояние участника и флаги микрофона, камеры и
экрана); уникальный частичный индекс держит один идущий звонок на беседу.

В DMParticipantProfiles сравнение с владельцем обёрнуто в COALESCE: у 1:1
dm_owner_id пуст, и NULL нельзя было прочитать в int — ручка звонка в
личной беседе падала на 500.

Тесты: жизненный цикл звонка в хранилище (гудки, active на втором
участнике, история, повторный звонок) и выборка сторожем.
This commit is contained in:
2026-09-26 17:02:51 +03:00
parent 96e0cd5918
commit 50709396cd
4 changed files with 696 additions and 2 deletions
+4 -2
View File
@@ -137,11 +137,13 @@ type DMParticipant struct {
IsCurrentUser bool
}
// DMParticipantProfiles возвращает участников беседы с профилями.
// DMParticipantProfiles возвращает участников беседы с профилями. Выражение
// «владелец» обёрнуто в COALESCE: у 1:1 dm_owner_id пуст, а сравнение с NULL
// даёт NULL, который нельзя прочитать в int.
func (s *Store) DMParticipantProfiles(ctx context.Context, channelID, currentUserID uint64) ([]DMParticipant, error) {
rows, err := s.reader.QueryContext(ctx, `
SELECT u.id, u.username, u.display_name, u.avatar_file_id, u.status, u.last_seen_at,
c.dm_owner_id = u.id
COALESCE(c.dm_owner_id = u.id, 0)
FROM dm_participants p
JOIN users u ON u.id = p.user_id
JOIN channels c ON c.id = p.channel_id