189 lines
13 KiB
Markdown
189 lines
13 KiB
Markdown
# Уведомления
|
||
|
||
Текущая система уведомлений — verb-based Django app с хранением в PostgreSQL и доставкой через WebSocket, email и Web Push.
|
||
|
||
## Источники истины
|
||
|
||
- models: `backend/notifications/models.py`
|
||
- создание: `backend/notifications/signals.py`
|
||
- channel routing: `backend/notifications/channels.py`
|
||
- REST API: `backend/notifications/api/`
|
||
- senders/tasks: `backend/notifications/senders/` и `backend/notifications/tasks/`
|
||
- realtime state events: `backend/notifications/realtime.py`
|
||
- frontend context: `frontend/src/contexts/NotificationsContext.tsx`
|
||
- Web Push hook: `frontend/src/hooks/useWebPush.ts`
|
||
- tests: `backend/tests/api/v1/notifications/` и `backend/tests/realtime/`
|
||
|
||
`NotificationService` в `backend/notifications/services.py` — deprecated stub и выбрасывает `NotImplementedError`. Новый код обязан использовать `notify.send(...)`.
|
||
|
||
## Создание уведомления
|
||
|
||
```python
|
||
from notifications.signals import notify
|
||
|
||
notify.send(
|
||
sender=request.user,
|
||
recipient=recipient,
|
||
verb="document_assigned",
|
||
target=document,
|
||
description="Вам назначен документ",
|
||
action_url=f"/documents/{document.pk}",
|
||
data={
|
||
"title": "Новый документ",
|
||
"document_id": document.pk,
|
||
},
|
||
)
|
||
```
|
||
|
||
`recipient` может быть одним `Employee` или списком/tuple сотрудников. Обязательны `recipient` и `verb`. `actor`, `action_object` и `target` хранятся через GenericForeignKey.
|
||
|
||
Не создавайте уведомления пользователю о его собственном действии, если продуктовый сценарий явно этого не требует. `action_url` должен быть внутренним разрешённым маршрутом frontend, а `data` — JSON-serializable.
|
||
|
||
Для сообщений verb `chat_mention` создаётся только по проверенному структурированному полю `Message.mentions`, а не по произвольному тексту после `@`. Получатель упоминания исключается из обычного `chat_new_message` или `chat_reply` для этого же сообщения, поэтому одно действие не создаёт два уведомления. Старое `notifications_enabled=false` мигрирует в режим `mentions`: обращения продолжают приходить. Явный режим `muted` и временная пауза подавляют все события чата.
|
||
|
||
## Поток доставки
|
||
|
||
1. `notify.send` создаёт `Notification`.
|
||
2. `post_save` проверяет `UserChannelPreferences`, disabled verb и DND.
|
||
3. Через `transaction.on_commit` запускаются Celery tasks.
|
||
4. WebSocket sender отправляет событие в `user_{id}`.
|
||
5. Email sender отправляет instant email либо digest.
|
||
6. Push sender выбирает активные устройства и маршруты приложений.
|
||
|
||
Если Celery broker недоступен, создание записи уведомления не откатывается: ошибка постановки задачи логируется. Повторная доставка требует отдельной операционной проверки.
|
||
|
||
## Модели
|
||
|
||
`Notification` содержит recipient, actor, verb, action object, target, description, action URL, JSON data, unread/deleted/public/emailed flags и timestamps. Удаление через API мягкое (`deleted=True`).
|
||
|
||
`UserChannelPreferences` управляет:
|
||
|
||
- `web_enabled`;
|
||
- `email_enabled` и `email_frequency`: `instant`, `daily`, `weekly`, `never`;
|
||
- `push_enabled`;
|
||
- DND interval;
|
||
- `disabled_verbs`.
|
||
|
||
При DND отправляется только silent WebSocket, если web channel включён. Email и push в этот момент не ставятся в очередь.
|
||
|
||
`NotificationInstallation` связывает Web Push subscription с приложением `main` или `messenger` и device group. `NotificationDeliveryRouting` выбирает приложение доставки по категориям: messages, tasks, requests, procurement, documents, guests, calendar, feed, other.
|
||
|
||
`ChatUserSettings` дополнительно управляет режимом конкретного чата (`all`, `mentions`, `muted`), `muted_until`, показом текста в системном уведомлении и звуком. Endpoint `POST /api/v1/communications/chats/{id}/notifications/` принимает эти поля; пустой POST сохранён как совместимый быстрый переключатель `all` ↔ `mentions`.
|
||
|
||
## REST API
|
||
|
||
Prefix: `/api/v1/notifications/`.
|
||
|
||
| Method и path | Назначение |
|
||
| --- | --- |
|
||
| `GET /` | Список; filters `page`, `page_size`, `verb`, `unread_only`, `search` |
|
||
| `GET /count/` | Общее количество непрочитанных |
|
||
| `GET /summary/` | Агрегаты unread для навигации и доменных экранов |
|
||
| `GET /verb-types/` | Доступные verbs и счётчики |
|
||
| `POST /{id}/read/` | Отметить одно прочитанным |
|
||
| `POST /{id}/unread/` | Вернуть в непрочитанные |
|
||
| `DELETE /{id}/` | Soft-delete |
|
||
| `POST /read-all/` | Прочитать все или один verb |
|
||
| `POST /category/read/` | Прочитать список verbs категории |
|
||
| `DELETE /delete-all-read/` | Soft-delete всех прочитанных |
|
||
| `GET, PUT /preferences/` | Настройки каналов |
|
||
| `GET /push/vapid-key/` | Публичный VAPID key |
|
||
| `POST /push/subscribe/` | Создать/обновить push subscription |
|
||
| `POST, DELETE /push/unsubscribe/` | Удалить одну или все subscriptions |
|
||
| `POST /push/installations/heartbeat/` | Обновить installation lease |
|
||
| `GET, PUT /push/delivery-routing/` | Маршрутизация main/messenger |
|
||
| `DELETE /push/installations/disconnect/` | Отключить приложение на устройстве |
|
||
|
||
Endpoint `/push/subscriptions/` отсутствует. Не используйте старые примеры с `/api/v2/notifications/` или `/ws/notifications/`.
|
||
|
||
## Realtime events
|
||
|
||
Через единый `/ws/` frontend получает:
|
||
|
||
- `notification` — новая запись;
|
||
- `unread_count` — актуальный общий счётчик;
|
||
- `notification_read` — прочтение в другой вкладке/сессии;
|
||
- `notifications_read_all` — массовое прочтение.
|
||
- `notifications_deleted` — удаление записей из всех открытых сессий.
|
||
|
||
`NotificationsContext` объединяет REST initial load и realtime delta, подавляет дубли и выполняет fallback refresh. Не поднимайте отдельный notifications WebSocket.
|
||
|
||
## Web Push
|
||
|
||
Backend требует `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY` и `VAPID_ADMIN_EMAIL`. Subscription сохраняется в `django-push-notifications` `WebPushDevice` и связывается с `NotificationInstallation`.
|
||
|
||
Frontend flow:
|
||
|
||
1. проверяет browser support и permission;
|
||
2. получает VAPID public key;
|
||
3. регистрирует service worker;
|
||
4. отправляет installation heartbeat;
|
||
5. только по user gesture запрашивает permission и создаёт subscription;
|
||
6. передаёт endpoint, `p256dh`, `auth`, installation/device group и app kind backend-у.
|
||
|
||
Push работает только в secure context, кроме localhost. Permission `denied` нельзя повторно запросить из JavaScript — пользователь должен изменить настройку сайта в браузере.
|
||
|
||
При permanent delivery error (`404`, `410`, expired/unregistered subscription) device деактивируется и отвязывается от installation. Transient error не удаляет устройство.
|
||
|
||
Для messenger-only route неуспешная доставка может fallback-нуться на main installation того же device group. Устаревшая messenger installation также возвращает доставку в main.
|
||
|
||
Уведомления сообщений используют стабильный `tag=chat-{chat_id}` и несут `chat_id`, `message_id`, `notification_id`. Backend отправляет невидимые control push-команды, а `frontend/public/sw.js` применяет их через `getNotifications()`: прочтение закрывает уведомление на остальных устройствах, удаление сообщения убирает относящийся к нему push, редактирование заменяет только уже показанный push. Если исходная Celery-задача задержалась, sender повторно проверяет `unread/deleted` и не показывает устаревшее уведомление.
|
||
|
||
Активный чат хранится как `ActiveChatSession` с TTL 75 секунд и продлевается WebSocket heartbeat. Presence используется только для подавления нативного Web Push уже открытого чата; она не отмечает notification прочитанным. Событие состояния notification продолжает приходить по WebSocket, а read state изменяется гарантированной HTTP-операцией вместе с курсором сообщения и затем синхронизируется между вкладками. После добавления или изменения Celery tasks worker требуется перезапустить — Django autoreload задачи Celery не обновляет.
|
||
|
||
## Email
|
||
|
||
`instant` ставит отдельную Celery task при создании уведомления. `daily` и `weekly` обрабатываются Celery Beat в 09:00 по timezone проекта. `never` отключает email.
|
||
|
||
Проверьте `EMAIL_BACKEND`, SMTP settings, `DEFAULT_FROM_EMAIL` и `SITE_URL`. В DEBUG по умолчанию используется console email backend.
|
||
|
||
## Настройки
|
||
|
||
```dotenv
|
||
REDIS_HOST=127.0.0.1
|
||
REDIS_PORT=6379
|
||
CELERY_BROKER_URL=redis://127.0.0.1:6379/0
|
||
CELERY_RESULT_BACKEND=redis://127.0.0.1:6379/1
|
||
|
||
VAPID_PUBLIC_KEY=change-me
|
||
VAPID_PRIVATE_KEY=change-me
|
||
VAPID_ADMIN_EMAIL=admin@example.com
|
||
SITE_URL=http://localhost:8000
|
||
```
|
||
|
||
Запуск worker и Beat:
|
||
|
||
```bash
|
||
cd backend
|
||
celery -A eusrr_backend worker -l info
|
||
celery -A eusrr_backend beat -l info
|
||
```
|
||
|
||
## Добавление нового verb
|
||
|
||
1. Выберите стабильное имя в `snake_case` с доменным prefix.
|
||
2. Добавьте translation/category mapping на frontend, если verb виден пользователю.
|
||
3. Передавайте title в `data.title`, если техническое имя verb не подходит как заголовок.
|
||
4. Передавайте ID доменного объекта в `data` для unread summary/navigation.
|
||
5. Не отправляйте секреты и большой payload.
|
||
6. Добавьте тест создания и routing по каналам.
|
||
7. Добавьте тест action URL и self-notification rule.
|
||
|
||
## Проверка
|
||
|
||
```bash
|
||
cd backend
|
||
pytest tests/api/v1/notifications tests/realtime -q
|
||
```
|
||
|
||
Для ручной проверки создайте уведомление через `notify.send`, затем проверьте REST list, Celery log, WebSocket event и push device. Не вызывайте sender напрямую: это обходит channel preferences, DND и transaction boundary.
|
||
|
||
## Диагностика
|
||
|
||
- Запись есть, realtime нет: проверьте Celery task `notifications.send_websocket_notification`, Redis channel layer и подписку на `user_{id}`.
|
||
- Push не создаётся: проверьте HTTPS/localhost, permission, service worker и VAPID public key.
|
||
- Push создан, но не приходит: проверьте `push_enabled`, active `WebPushDevice`, installation route и Celery worker.
|
||
- Уведомление дублируется: найдите повторный `notify.send`; UI deduplication не должна маскировать backend double-create.
|
||
- Unread count устарел: операции read должны идти через API/model methods, которые инвалидируют cache и отправляют realtime state event.
|
||
- Email не приходит: проверьте frequency, DND, SMTP и поле `emailed`.
|