618 lines
38 KiB
Markdown
618 lines
38 KiB
Markdown
# Бизнес-постановка: Обмен сообщениями (Chat / Dialog)
|
||
|
||
**Статус:** v1 — консолидация принятых решений из `HAN_chat_specification` (`arch-00`…`arch-05`, `module-01`, `module-05`, `module-06`); открытые вопросы зафиксированы в §17
|
||
**Продукт:** HAN Chat (клиентское приложение + `api-backend` + `message-safety` + `bitrix-local-app` / Open Lines)
|
||
**Источники:** архитектура `HAN_chat_specification`; макет Figma (**не канон** — только визуализация; при расхождении приоритет у этого ТЗ и arch-документов)
|
||
**Связанный backlog:** непрочитанные сообщения на кнопке «Чат» (синхронизация между устройствами); UX ошибки отложенного сообщения после входа
|
||
**Смежно:** пользователь (auth/bootstrap), уведомления (`send_chat_message`, кнопки Чат/Оператор), популярные вопросы, Message Safety
|
||
|
||
**Нормативная часть — §1–§16.** §17 — ненормативный журнал решений и открытых вопросов; при расхождении с §1–§16 приоритет у §1–§16. При расхождении этого документа с arch/module после их обновления — приоритет у arch/module до синхронизации.
|
||
|
||
---
|
||
|
||
## 1. Цель
|
||
|
||
Дать клиенту канал диалога с оператором компании: отправка текста и файлов из приложения в Bitrix24 Open Lines и получение ответов оператора в реальном времени — с проверкой исходящих сообщений (Message Safety), идемпотентностью, ownership и деградацией без потери уже принятых данных.
|
||
|
||
Чат — канал **«клиент ↔ оператор»**. Уведомления — канал **«компания → клиент»** вне чата (см. `notification-requirements.md`). Непрочитанные ответы оператора **не** моделируются как уведомления; индикатор непрочитанного чата — отдельная фича (§3.2, §17.2).
|
||
|
||
---
|
||
|
||
## 2. Два направления обмена
|
||
|
||
| Направление | `sender_type` | Кто инициирует | Проверка Message Safety | Доставка |
|
||
|---|---|---|---|---|
|
||
| **C→O. Клиент → оператор** | `client` | Frontend (JWT) | **Обязательна** до Open Lines | `api-backend` → `bitrix-local-app` → Open Lines |
|
||
| **O→C. Оператор → клиент** | `company` | Bitrix24 webhook → `bitrix-local-app` → inbox API | **Нет** outbound moderation (доверенный канал); MIME/size/antivirus policy модуля | App DB + WS / polling |
|
||
|
||
Правила:
|
||
|
||
1. Чат доступен **только авторизованному** клиенту (JWT + `bootstrap`). Гость инициирует auth; текст сохраняется локально и отправляется после входа (§6.4).
|
||
2. У пользователя **не более одного активного** диалога (`open` \| `waiting_for_company` \| `waiting_for_client`).
|
||
3. `dialog_id` приложения **равен** `external_chat_id` для Open Lines.
|
||
4. MVP: одно исходящее сообщение — либо текст, либо ровно один файл (`content_kind`), не оба сразу.
|
||
5. Клиент на `POST .../messages` получает **только финальный** результат (или ошибку инфраструктуры), не промежуточное «обрабатывается».
|
||
6. Источник истины ленты — App DB; WS — at-most-once best effort; после reconnect — REST reconcile.
|
||
|
||
---
|
||
|
||
## 3. Границы релиза
|
||
|
||
### 3.1. В scope
|
||
|
||
- Один активный диалог на пользователя; ленивое создание перед первым сообщением.
|
||
- Исходящие: `content_kind` `text` \| `file`; входящие: текст и файлы оператора.
|
||
- Orchestration Message Safety: sync check + sync-wait poll `task_id` + checkpoint `safety_tasks` + recovery.
|
||
- Presigned upload клиента в S3-quarantine → complete → safety → promote в S3-data attachments.
|
||
- Durable outbox App → Open Lines; idempotent delivery по `message_id`.
|
||
- Inbox Open Lines → App: `message.new`, `dialog.closed`; idempotency по `(external_chat_id, bitrix_message_id)` / `event_id`.
|
||
- Realtime `WS /api/v1/realtime` + polling fallback истории сообщений.
|
||
- Популярные вопросы как обычная отправка текста после auth.
|
||
- Общий механизм **отложенного сообщения** на frontend (ручной ввод, популярный вопрос, CTA уведомлений `send_chat_message`).
|
||
- Кнопки UI «Чат» и «Оператор» (`tel:` ← `operator.call.phone`) — смежно с уведомлениями.
|
||
- Нормализация текста входящих из Bitrix (удаление служебной разметки отправителя) — уже закрытый дефект backlog.
|
||
- Ownership, rate limits, `Idempotency-Key` (TTL 24 ч) на create dialog / send message.
|
||
- Константы `chat.attachments.*`, `rate_limit.message_send.*`, `operator.call.phone`.
|
||
|
||
### 3.2. Вне scope
|
||
|
||
- Смешанное сообщение «текст + файл(ы)» (post-MVP, отдельная версия API).
|
||
- Несколько вложений в одном исходящем сообщении.
|
||
- UI-раздел «История чатов / список диалогов» как продуктовый экран — **deprecated** для MVP frontend (один чат с компанией); backend `GET /api/v1/dialogs` **не удаляется**.
|
||
- Индикатор непрочитанных сообщений чата и sync между устройствами (`Dialog.client_last_opened_at` и аналоги) — backlog п.23–24; **не** путать с бейджем уведомлений.
|
||
- Typing indicators, реакции, редактирование/удаление сообщений клиентом, ответы на конкретное сообщение (quote/reply).
|
||
- Голосовые / видеосообщения, стикеры, произвольные форматы сверх `chat.attachments.*`.
|
||
- Push / deep link в чат (модель может быть push-ready позже).
|
||
- Админ-модерация очереди `needs_review` (enum зарезервирован, в MVP не создаётся).
|
||
- CRM Contact sync в hot path чата (`bitrix-sync` **не** участвует в Open Lines delivery).
|
||
- Реальная антивирус/LLM-модерация: в MVP допускается stub `message-safety` с фиксированными правилами теста; канонический контракт вердиктов — arch-02 (`200/203/403`).
|
||
|
||
---
|
||
|
||
## 4. UI
|
||
|
||
| Место | Поведение |
|
||
|---|---|
|
||
| Главная: поле ввода | Отправка текста; без auth → согласия → OTP → отложенная отправка |
|
||
| Главная: популярные вопросы | Тап = автоотправка текста вопроса (тот же поток, что ручной ввод) |
|
||
| Кнопка «Чат» | Открывает текущий активный диалог или создаёт его при отсутствии |
|
||
| Кнопка «Оператор» | `tel:` на `operator.call.phone` (не чат-сообщение) |
|
||
| Экран чата | Лента сообщений клиента и компании; статусы доставки по DTO |
|
||
| Вложения | Выбор файла → init → PUT в quarantine → complete → send `content_kind=file` |
|
||
| Гость в Центре уведомлений / чате | Auth-gate; после входа — ЛК |
|
||
|
||
Визуал — по Figma. Figma не канон статусов доставки и состава API.
|
||
|
||
---
|
||
|
||
## 5. Бизнес-модель
|
||
|
||
### 5.1. Сущности
|
||
|
||
| Сущность | Схема | Назначение |
|
||
|---|---|---|
|
||
| `Dialog` | `han_app` | Диалог клиента с Open Lines |
|
||
| `Message` | `han_app` | Сообщение в диалоге |
|
||
| `MessageAttachment` | `han_app` | Вложение (client upload или company inbound) |
|
||
| `safety_tasks` | `han_app` | Checkpoint sync-wait Message Safety (техническая) |
|
||
| `delivery_outbox` | `han_app` | Durable намерение доставки в Open Lines |
|
||
| `openlines_inbox_receipts` | `han_app` | Idempotency приёма событий local app |
|
||
| `dialog_sessions` | `bitrix_local` | Маппинг чата у `bitrix-local-app` |
|
||
| `popular_questions` | `han_app` | Справочник текстов быстрых вопросов (не сообщения) |
|
||
|
||
### 5.2. `Dialog.status`
|
||
|
||
| Значение | Смысл |
|
||
|---|---|
|
||
| `open` | Диалог создан, сообщений ещё нет |
|
||
| `waiting_for_company` | Последнее значимое — исходящее от клиента; ждём оператора |
|
||
| `waiting_for_client` | Последнее значимое — входящее от оператора; ждём клиента |
|
||
| `closed` | Закрыт в Open Lines (`dialog.closed` / `ONIMCONNECTORDIALOGFINISH`) |
|
||
|
||
Переходы:
|
||
|
||
| Событие | Новый статус |
|
||
|---|---|
|
||
| `POST /dialogs` (новый) | `open` |
|
||
| Успешная доставка исходящего клиента в Open Lines | `waiting_for_company` |
|
||
| Сохранено входящее от оператора | `waiting_for_client` |
|
||
| Inbox `dialog.closed` | `closed` |
|
||
|
||
Из `closed` обратный переход **запрещён**. Новый активный диалог после закрытия — снова через `POST /dialogs`, когда продукт это разрешит; инвариант «один active» сохраняется.
|
||
|
||
### 5.3. Сообщение: вид и статусы
|
||
|
||
**`content_kind` (исходящее MVP):**
|
||
|
||
| `content_kind` | Тело запроса | `Message.text` | Вложения |
|
||
|---|---|---|---|
|
||
| `text` | непустой `text` | текст | 0 |
|
||
| `file` | `attachment_id` + `checksum` | пустая строка | ровно 1 |
|
||
|
||
Запрещено: текст + файл → `400 mixed_content_not_allowed`; пустое → `400 empty_message`; >1 вложение → `400 too_many_attachments`.
|
||
|
||
**`sender_type`:** `client` \| `company`.
|
||
|
||
**`safety_status`:**
|
||
|
||
| Значение | Смысл |
|
||
|---|---|
|
||
| `pending` | Проверка не завершена |
|
||
| `allowed` | Финальный allow |
|
||
| `blocked` | Финальный deny |
|
||
| `needs_review` | Зарезервирован, в MVP не создаётся |
|
||
|
||
Входящие `company` всегда `safety_status=allowed`.
|
||
|
||
**`delivery_status`:**
|
||
|
||
| Значение | Смысл | Клиенту как финал `POST .../messages`? |
|
||
|---|---|---|
|
||
| `accepted` | Принято API, safety/доставка ещё не финализированы | нет (промежуточный) |
|
||
| `processing` | Sync-wait safety | нет |
|
||
| `delivered` | Allow + ушло в Open Lines (или входящее сохранено) | да |
|
||
| `rejected` | Deny Message Safety | да (`422 message_blocked`) |
|
||
| `failed` | Инфраструктурная ошибка (Bitrix/S3/timeout), не safety-deny | да (`503`/`504`) |
|
||
|
||
Инварианты: `rejected` ↔ `blocked`; `delivered` → `allowed`.
|
||
|
||
### 5.4. Вложение
|
||
|
||
| `scan_status` | Смысл |
|
||
|---|---|
|
||
| `pending` | В quarantine, проверка не завершена |
|
||
| `clean` | Allow, файл в S3-data (после promote) |
|
||
| `infected` | Deny |
|
||
| `failed` | Ошибка инфраструктуры проверки |
|
||
|
||
`direction`: `client_upload` \| `company_inbound`.
|
||
|
||
Клиентский файл до allow живёт **только** в S3-quarantine. Постоянных access keys у клиента нет — только короткий presigned PUT/GET.
|
||
|
||
### 5.5. Идентификаторы
|
||
|
||
| Имя | Назначение |
|
||
|---|---|
|
||
| `dialog_id` | UUID диалога = `external_chat_id` Open Lines |
|
||
| `message_id` | UUID сообщения; ключ idempotent delivery |
|
||
| `attachment_id` | UUID вложения |
|
||
| `bitrix_message_id` | ID сообщения в Bitrix для inbox dedup |
|
||
| `task_id` | Async-проверка Message Safety |
|
||
| `Idempotency-Key` | Клиентский ключ create dialog / send (Redis + durable fallback), TTL 24 ч |
|
||
|
||
### 5.6. Константы (`app_settings`)
|
||
|
||
| Ключ | Смысл | Seed |
|
||
|---|---|---|
|
||
| `chat.attachments.allowed_extensions` | Разрешённые расширения | jpg,jpeg,png,webp,heic,heif,pdf |
|
||
| `chat.attachments.allowed_mime_types` | Разрешённые MIME | image/jpeg, image/png, …, application/pdf |
|
||
| `chat.attachments.disallowed_extensions` | Явный deny-list | svg,doc,docx,xls,xlsx,csv |
|
||
| `chat.attachments.max_size_mb` | Макс. размер | `5` |
|
||
| `chat.attachments.storage` | Провайдер | `selectel_s3` |
|
||
| `chat.attachments.upload_mode` | Режим | `presigned_put` |
|
||
| `chat.attachments.safety_scan_required` | Safety обязателен | `true` |
|
||
| `chat.attachments.presigned_upload_ttl_seconds` | TTL upload URL | `600` |
|
||
| `rate_limit.message_send.per_user` | Лимит отправок | `30/minute` |
|
||
| `rate_limit.message_send.per_dialog` | Лимит на диалог | `20/minute` |
|
||
| `rate_limit.download_url.per_user` | Лимит download-url | `60/hour` |
|
||
| `operator.call.phone` | Телефон кнопки «Оператор» | `+74999591007` |
|
||
|
||
Те же `chat.attachments.*` переиспользуются уведомлениями для документов клиента.
|
||
|
||
### 5.7. Популярные вопросы
|
||
|
||
- Справочник `popular_questions` отдаётся public content API.
|
||
- Отдельного backend-flow нет: после auth текст уходит как обычное `content_kind=text`.
|
||
- Без auth — тот же механизм отложенного сообщения, что у ручного ввода.
|
||
|
||
---
|
||
|
||
## 6. Поведение UI и сценарии
|
||
|
||
### 6.1. Создание / открытие диалога
|
||
|
||
1. Frontend перед первым `POST .../messages` вызывает `POST /api/v1/dialogs` с `Idempotency-Key`.
|
||
2. Нет active → `201`, `status=open`.
|
||
3. Есть active → `200`, возвращается существующий (новый не создаётся).
|
||
4. Кнопка «Чат» открывает этот диалог (или создаёт при отсутствии).
|
||
|
||
### 6.2. Исходящее текстовое сообщение
|
||
|
||
1. `POST /dialogs` (если нет `dialog_id`).
|
||
2. `POST .../messages` с `content_kind=text`, `Idempotency-Key`.
|
||
3. Rate limits (nginx + app).
|
||
4. Message Safety (текст, ссылки).
|
||
5. Allow → outbox → Open Lines → `delivery_status=delivered`, `Dialog` → `waiting_for_company`.
|
||
6. Deny → `422 message_blocked`, в Open Lines **не** уходит.
|
||
7. Dependency failure → `503`/`504`, при уже созданном Message — `delivery_status=failed`.
|
||
|
||
### 6.3. Исходящий файл
|
||
|
||
1. `POST .../attachments/init` → presigned PUT в **S3-quarantine**.
|
||
2. Frontend грузит байты **напрямую в S3** (не через api-backend).
|
||
3. `POST .../attachments/{id}/complete` + checksum → HeadObject, metadata, `scan_status=pending`.
|
||
4. `POST .../messages` с `content_kind=file`, `attachment_id`, `checksum`.
|
||
5. Safety (файл); при `203 pending` api-backend sync-poll `task_id` внутри того же HTTP-запроса клиента.
|
||
6. Allow → promote quarantine → S3-data attachments → delivery Open Lines (`message.files` signed URL, `message.text` пустой).
|
||
7. Deny → quarantine delete, `blocked`/`rejected`.
|
||
|
||
Пока идёт poll safety, **это** клиентское соединение ждёт; параллельные запросы других клиентов не блокируются.
|
||
|
||
### 6.4. Отложенное сообщение (гость → после auth)
|
||
|
||
Общий frontend-механизм для:
|
||
|
||
- ручного ввода;
|
||
- популярного вопроса;
|
||
- CTA уведомлений с `send_chat_message` (после входа в контуре G).
|
||
|
||
Порядок:
|
||
|
||
1. Клиент инициирует отправку без JWT → согласия → OTP → `bootstrap` → `session-start`.
|
||
2. Экран авторизации **завершается после успешного bootstrap**, даже если последующая отправка в чат упадёт.
|
||
3. Затем `POST /dialogs` → `POST .../messages` с сохранённым текстом.
|
||
4. Ошибка Bitrix/safety показывается **в контексте чата**, не как «не удалось завершить вход» (backlog п.21).
|
||
|
||
### 6.5. Входящее от оператора
|
||
|
||
1. Bitrix24 `ONIMCONNECTOR*` → `bitrix-local-app` (inbox, retry, DLQ).
|
||
2. Forward в `POST /internal/openlines/v1/inbox` (`message.new`).
|
||
3. api-backend: ownership по `external_chat_id`, save Message `company`/`allowed`/`delivered`, файлы → S3-data + `MessageAttachment`.
|
||
4. Текст очищается от служебной разметки отправителя Bitrix (BBCode-префиксы имени и т.п.) — клиент видит чистый текст ответа.
|
||
5. `Dialog.status` → `waiting_for_client`.
|
||
6. Publish WS `message.new`; при недоступности WS — клиент подтянет через polling.
|
||
7. Local app ack delivery в Bitrix после успешного apply / duplicate-ack.
|
||
|
||
Лимиты MIME/size для файлов оператора в MVP — те же `chat.attachments.*`.
|
||
|
||
### 6.6. Закрытие диалога
|
||
|
||
Inbox `dialog.closed` → `Dialog.status=closed`. Frontend получает `dialog.status` по WS или при следующем GET. Новый активный — только новым `POST /dialogs`.
|
||
|
||
### 6.7. Realtime и fallback
|
||
|
||
1. `WS /api/v1/realtime` + JWT.
|
||
2. После `connected` — `subscribe` с `dialog_ids` (и опционально `notifications`).
|
||
3. События чата: `message.new`, `message.status`, `dialog.status`.
|
||
4. Reconnect: backoff 1s…30s; повтор `subscribe`.
|
||
5. WS недоступен > 30s → polling `GET .../messages?after=<cursor>` (и notifications counter при подписке).
|
||
6. Ping/pong ~30s.
|
||
|
||
### 6.8. Скачивание вложения
|
||
|
||
`GET .../attachments/{id}/download-url` → короткий presigned GET + audit `attachment.download_url_issued`. URL в логи/audit не пишется. Ownership обязателен.
|
||
|
||
---
|
||
|
||
## 7. Матрицы поведения
|
||
|
||
### 7.1. Исходящее: safety → delivery
|
||
|
||
| Вердикт safety | `safety_status` | `delivery_status` (финал клиенту) | Open Lines | HTTP клиенту |
|
||
|---|---|---|---|---|
|
||
| `200 allow` | `allowed` | `delivered` после успешной отправки; иначе `failed` | да (после allow) | `201` или `503`/`504` |
|
||
| `403 deny` | `blocked` | `rejected` | нет | `422 message_blocked` |
|
||
| `203 pending` → затем allow/deny | как финал | как финал | только после allow | финальный код после poll |
|
||
| timeout / circuit open | по политике модуля | `failed` | нет | `503`/`504` |
|
||
|
||
Клиенту **не** отдаётся промежуточный `processing` как успешный ответ `POST .../messages`.
|
||
|
||
### 7.2. Идемпотентность клиента
|
||
|
||
| Ситуация | Результат |
|
||
|---|---|
|
||
| Тот же `Idempotency-Key` + то же тело | Тот же HTTP-ответ, без повторного side-effect |
|
||
| Тот же ключ + другое тело | `409 idempotency_key_reused` |
|
||
| Нет ключа на обязательном endpoint | `400 validation_error` |
|
||
| TTL | 24 часа (Redis); durable fallback в `idempotency_records` |
|
||
|
||
Доставка в Open Lines идемпотентна по `message_id`. Inbox `message.new` — unique `(external_chat_id, bitrix_message_id)`.
|
||
|
||
### 7.3. Деградация зависимостей
|
||
|
||
| Зависимость | Влияние на чат |
|
||
|---|---|
|
||
| Redis DB0 | Write fail-closed; read ограниченно деградирует |
|
||
| Redis DB1 (realtime) | REST работает; WS/publish деградирует → polling |
|
||
| `message-safety` | Отправка недоступна; чтение истории работает |
|
||
| `bitrix-local-app` / Bitrix | После allow сообщение может стать `failed`; recovery по outbox |
|
||
| S3 | Файловые операции недоступны; текстовый чат продолжает работать |
|
||
| Open Lines в readiness | Может быть `degraded`; не обязано валить весь API |
|
||
|
||
### 7.4. Ownership и ошибки доступа
|
||
|
||
| Ситуация | Код |
|
||
|---|---|
|
||
| Чужой `dialog_id` / `attachment_id` / `message` | `404` (существование не раскрывается) |
|
||
| Нет/невалиден JWT | `401` |
|
||
| Диалог `closed`, политика запрещает send | по контракту модуля (`409`/`422` — уточнить в OpenAPI при публикации) |
|
||
| Safety deny | `422 message_blocked` |
|
||
|
||
---
|
||
|
||
## 8. Идентификация и корреляция
|
||
|
||
- Публичные id — UUID.
|
||
- `dialog_id` генерирует приложение при create и передаётся в Open Lines как `external_chat_id`.
|
||
- При первой доставке `bitrix-local-app` создаёт/обновляет `dialog_sessions`.
|
||
- `X-Request-ID`, `traceparent`, опционально `X-Ux-Session-Id` — для логов/audit; не auth.
|
||
- Connector Bitrix: `han_mobile_app`, Open Line id по env/глоссарию.
|
||
|
||
---
|
||
|
||
## 9. API
|
||
|
||
Общие конвенции — arch-02: `/api/v1/*` JWT, `/api/v1/public/*`, `/internal/{mnemonic}/v1/*`; JSON snake_case; даты RFC 3339 UTC; cursors opaque.
|
||
|
||
### 9.1. Клиентские (JWT)
|
||
|
||
| Метод и путь | Назначение |
|
||
|---|---|
|
||
| `POST /api/v1/dialogs` | Find-or-create active dialog (`Idempotency-Key`) |
|
||
| `GET /api/v1/dialogs` | Список/история диалогов (API есть; UI MVP — один чат) |
|
||
| `GET /api/v1/dialogs/{dialog_id}` | Карточка диалога |
|
||
| `GET /api/v1/dialogs/{dialog_id}/messages?after=&limit=` | История / polling |
|
||
| `POST /api/v1/dialogs/{dialog_id}/messages` | Отправка (`Idempotency-Key` + safety) |
|
||
| `POST .../attachments/init` | Presigned PUT quarantine |
|
||
| `POST .../attachments/{id}/complete` | Подтверждение upload |
|
||
| `GET .../attachments/{id}/download-url` | Presigned GET + audit |
|
||
| `WS /api/v1/realtime` | События чата (и опционально уведомлений) |
|
||
|
||
### 9.2. Public
|
||
|
||
| Метод и путь | Назначение |
|
||
|---|---|
|
||
| `GET /api/v1/public/content` | Тексты + `popular_questions` |
|
||
| `GET /api/v1/public/settings` | В т.ч. публичные флаги/лимиты UI при `is_public` |
|
||
|
||
### 9.3. Internal
|
||
|
||
| Метод и путь | Кто → кто | Назначение |
|
||
|---|---|---|
|
||
| `POST /internal/safety/v1/messages/check` | api-backend → message-safety | Проверка |
|
||
| `GET /internal/safety/v1/messages/tasks/{task_id}` | api-backend → message-safety | Poll вердикта |
|
||
| `POST /internal/openlines/v1/messages` | api-backend → bitrix-local-app | Исходящая доставка |
|
||
| `GET /internal/openlines/v1/dialogs/{external_chat_id}` | api-backend → bitrix-local-app | Reconciliation |
|
||
| `POST /internal/openlines/v1/inbox` | bitrix-local-app → api-backend | Входящие события |
|
||
|
||
### 9.4. Ошибки домена чата
|
||
|
||
| Код | HTTP | Когда |
|
||
|---|---|---|
|
||
| `validation_error` | 400 | Нет Idempotency-Key, невалидное тело |
|
||
| `empty_message` | 400 | Нет текста и вложения |
|
||
| `mixed_content_not_allowed` | 400 | Текст и файл вместе |
|
||
| `too_many_attachments` | 400 | >1 вложение |
|
||
| `attachment_not_completed` | 400 | Send до complete |
|
||
| `attachment_checksum_mismatch` | 400 | Checksum не совпал |
|
||
| `unauthorized` | 401 | JWT |
|
||
| `message_blocked` | 422 | Safety deny |
|
||
| `idempotency_key_reused` | 409 | Ключ с другим телом |
|
||
| `resource_state_conflict` | 409 | Конфликт состояния (напр. complete с другим checksum) |
|
||
| `rate_limit_exceeded` | 429 | Лимит |
|
||
| `dependency_unavailable` | 503 | Circuit / недоступна зависимость |
|
||
| `dependency_timeout` | 504 | Timeout зависимости |
|
||
|
||
### 9.5. DTO `MessageResponse` (REST и WS)
|
||
|
||
```json
|
||
{
|
||
"message_id": "uuid",
|
||
"dialog_id": "uuid",
|
||
"sender_type": "client",
|
||
"content_kind": "text",
|
||
"text": "Здравствуйте",
|
||
"attachments": [],
|
||
"safety_status": "allowed",
|
||
"delivery_status": "delivered",
|
||
"created_at": "2026-07-09T12:00:00Z"
|
||
}
|
||
```
|
||
|
||
Сортировка сообщений: `created_at ASC` (append в ленте). Диалоги: `updated_at DESC`.
|
||
|
||
---
|
||
|
||
## 10. Модель данных (схема `han_app`)
|
||
|
||
Общие правила — module-01 §9.1 / arch-05.
|
||
|
||
### 10.1. `dialogs`
|
||
|
||
| Поле | Описание |
|
||
|---|---|
|
||
| `id` | = `dialog_id` = `external_chat_id` |
|
||
| `user_id` | FK владельца |
|
||
| `status` | enum §5.2 |
|
||
| `last_message_at` | |
|
||
| `closed_at` | при `closed` |
|
||
| common fields | обязательны |
|
||
|
||
Partial unique: один active dialog на `user_id`.
|
||
|
||
### 10.2. `messages`
|
||
|
||
| Поле | Описание |
|
||
|---|---|
|
||
| `id` | `message_id` |
|
||
| `dialog_id` | FK |
|
||
| `sender_type` | `client` \| `company` |
|
||
| `content_kind` | `text` \| `file` |
|
||
| `text` | непустой для text; `''` для file |
|
||
| `safety_status` / `delivery_status` | §5.3 |
|
||
| `external_message_id` | Bitrix id для inbound |
|
||
| `client_idempotency_key` | опционально/связка с Idempotency-Key |
|
||
| `occurred_at` | |
|
||
| common fields | |
|
||
|
||
Индексы: лента `(dialog_id, created_at, id)`; recovery по `delivery_status`; unique inbound `(dialog_id, external_message_id)`.
|
||
|
||
**Решение:** исходящее создаётся до завершения safety со статусами `pending`/`accepted`, чтобы `safety_tasks` имел FK (crash checkpoint).
|
||
|
||
### 10.3. `message_attachments`
|
||
|
||
Поля: `id`, `dialog_id`, `message_id NULL` до привязки, `owner_user_id`, `direction`, имена/MIME/size/checksum, `scan_status`, storage keys (working + quarantine), timestamps, common fields.
|
||
|
||
MVP: unique partial — не более одного active attachment на `message_id`.
|
||
|
||
### 10.4. Технические таблицы
|
||
|
||
| Таблица | Назначение | Физическая очистка |
|
||
|---|---|---|
|
||
| `safety_tasks` | Poll/recovery Message Safety | да, по retention |
|
||
| `delivery_outbox` | App → Open Lines | по статусам/retention модуля |
|
||
| `openlines_inbox_receipts` | Dedup inbox | по политике модуля |
|
||
| `idempotency_records` | Durable fallback Idempotency-Key | по `expires_at` |
|
||
|
||
### 10.5. Схема ключей S3 (чат)
|
||
|
||
```text
|
||
quarantine/... # клиентский upload до вердикта
|
||
attachments/... # проверенные вложения чата (client + company)
|
||
```
|
||
|
||
Бакет documents (`han-chat-documents`) — для документов профиля/уведомлений, **не** hot path чата Open Lines.
|
||
|
||
---
|
||
|
||
## 11. Фоновые процессы
|
||
|
||
| Процесс | Владелец | Назначение |
|
||
|---|---|---|
|
||
| Safety recovery worker | api-backend | Доводит `203 pending` после обрыва клиентского HTTP |
|
||
| Delivery outbox worker | api-backend | Retry доставки в local app / Open Lines |
|
||
| Quarantine orphan cleanup | api-backend / ops | Удаляет просроченные объекты без active task/attachment |
|
||
| Inbox retry / DLQ | bitrix-local-app | Надёжность webhook → API |
|
||
| Realtime publish | api-backend + Redis DB1 | Fan-out WS; при сбое — polling |
|
||
|
||
Application-код **не** обходит outbox «в обход» для повторной доставки без idempotency.
|
||
|
||
---
|
||
|
||
## 12. Audit и observability
|
||
|
||
| `event_type` | Когда |
|
||
|---|---|
|
||
| `dialog.created` | Новый диалог |
|
||
| `message.submitted` | Принято к обработке |
|
||
| `message.blocked` | Safety deny |
|
||
| `message.delivered` | Успех Open Lines / inbound saved |
|
||
| `message.failed` | Инфраструктурный fail |
|
||
| `attachment.upload_initialized` / `completed` / `promoted` / `rejected` | Lifecycle файла |
|
||
| `attachment.download_url_issued` | Presigned GET |
|
||
| `openlines.inbox_applied` | Входящее применено |
|
||
|
||
Метрики: latency send, safety poll duration/timeout, outbox depth/age/DLQ, inbox duplicate/apply, WS reconnect/drop, rate limit rejects. **Нельзя** использовать `user_id`/`dialog_id` как metric labels.
|
||
|
||
В логах нет: тел сообщений, tokens, presigned URL, Bitrix download URL, полного PII.
|
||
|
||
---
|
||
|
||
## 13. Хранение
|
||
|
||
- `Dialog` / `Message` / `MessageAttachment` — прикладные строки, soft-delete, физическое удаление запрещено.
|
||
- Закрытые диалоги и их сообщения остаются в БД (история API); UI MVP показывает текущий чат.
|
||
- Quarantine и технические checkpoint — исключения с retention.
|
||
- S3-data attachments живут с записью вложения; lifecycle бакетов — ops-политика.
|
||
|
||
---
|
||
|
||
## 14. Смежные сервисы
|
||
|
||
| Компонент | Роль в чате |
|
||
|---|---|
|
||
| **Frontend** | UI чата, отложенное сообщение, upload, WS/polling, кнопки Чат/Оператор |
|
||
| **api-backend** | Dialogs/messages/attachments, safety orchestration, outbox, inbox apply, realtime |
|
||
| **message-safety** | Вердикт allow/deny/pending по исходящим |
|
||
| **bitrix-local-app** | Connector Open Lines, исходящие/входящие, `dialog_sessions` |
|
||
| **Bitrix24 Open Lines** | Рабочее место оператора |
|
||
| **S3** | Quarantine + attachments |
|
||
| **Redis** | Rate limit, idempotency cache, realtime coordination |
|
||
| **nginx** | `/api/*` + WS upgrade; `proxy_read_timeout` ≥ safety poll budget + запас |
|
||
| **bitrix-sync** | **Не** в hot path чата |
|
||
|
||
Порядок работ (если поднимать домен с нуля): (1) dialogs + messages text path + idempotency → (2) safety orchestration → (3) Open Lines out + inbox → (4) attachments → (5) WS + polling → (6) recovery/outbox hardening.
|
||
|
||
---
|
||
|
||
## 15. Влияние на arch-документы
|
||
|
||
| Документ | Содержание по чату |
|
||
|---|---|
|
||
| `arch-00-glossary.md` | `Dialog`, `Message`, статусы, Open Lines terms |
|
||
| `arch-01-system-architecture.md` | Потоки C→O и O→C, создание диалога |
|
||
| `arch-02-api-contracts.md` | REST/WS/safety/openlines контракты |
|
||
| `arch-03-docker-compose-blueprint.md` | Сервисы, WS location, timeouts |
|
||
| `arch-04-settings-and-content.md` | `chat.attachments.*`, rate limits, operator phone |
|
||
| `module-01-api-backend.md` | Алгоритмы, таблицы, workers |
|
||
| `module-05-message-safety.md` | Stub/production safety |
|
||
| `module-06-bitrix-local-app.md` | Connector / inbox / out |
|
||
|
||
Этот документ собирает бизнес-смысл обмена сообщениями для аналитики и смежных фич; детальные алгоритмы — в module-спеках.
|
||
|
||
---
|
||
|
||
## 16. Критерии приёмки
|
||
|
||
1. Без JWT отправить сообщение нельзя; после auth отложенный текст/популярный вопрос уходит штатным `POST /dialogs` → `POST .../messages`.
|
||
2. Не более одного active dialog; повторный `POST /dialogs` возвращает существующий.
|
||
3. Text и file взаимоисключающи; mixed/empty/too many → соответствующие `400`.
|
||
4. `POST .../messages` возвращает только финальный статус; deny → `422 message_blocked` без доставки в Bitrix.
|
||
5. Allow → сообщение видно оператору в Open Lines; `Dialog` → `waiting_for_company`.
|
||
6. Ответ оператора появляется в клиенте через WS или polling; `Dialog` → `waiting_for_client`; текст без служебной разметки имени из Bitrix.
|
||
7. `dialog.closed` закрывает диалог; из `closed` нельзя вернуться тем же id.
|
||
8. Файлы: quarantine → safety → promote; deny чистит quarantine; download только presigned + audit.
|
||
9. Идемпотентность create/send соблюдается 24 ч; повтор delivery/inbox не плодит дубли.
|
||
10. При падении WS > 30s клиент уходит в polling и не теряет сообщения, уже лежащие в App DB.
|
||
11. Недоступность safety блокирует send, но не чтение истории; недоступность S3 не ломает text-only чат.
|
||
12. Ошибка отправки после успешного bootstrap не выглядит как ошибка входа.
|
||
13. Кнопка «Оператор» берёт номер только из `operator.call.phone`.
|
||
14. Популярный вопрос не имеет отдельного API — только text message.
|
||
15. Ownership: чужие dialog/attachment → `404`.
|
||
|
||
---
|
||
|
||
## 17. Журнал решений и открытых вопросов (ненормативно)
|
||
|
||
### 17.1. Принятые решения
|
||
|
||
| # | Решение | Раздел / источник |
|
||
|---|---|---|
|
||
| D1 | Чат ≠ уведомления; непрочитанный чат не есть Notification | §1, notification-requirements |
|
||
| D2 | Один active dialog; `dialog_id` = `external_chat_id` | §2, arch-01 |
|
||
| D3 | MVP content: text XOR file | §5.3, arch-02 |
|
||
| D4 | Клиент ждёт финальный вердикт на одном HTTP; poll safety внутри api-backend | §6.2–§6.3 |
|
||
| D5 | Исходящие проходят Message Safety; входящие оператора — доверенный канал | §2, arch-02 |
|
||
| D6 | Durable outbox + idempotent delivery; inbox dedup | §6, module-01 |
|
||
| D7 | WS обязателен как primary realtime; polling — fallback | §6.7 |
|
||
| D8 | UI истории диалогов deprecated; API списка сохраняется | §3.2 |
|
||
| D9 | Отложенное сообщение — общий frontend-механизм | §6.4 |
|
||
| D10 | Популярный вопрос = обычный text send | §5.7 |
|
||
| D11 | Presigned upload напрямую в S3; api-backend не проксирует байты | §6.3 |
|
||
| D12 | `chat.attachments.*` — единые лимиты для чата (и reuse уведомлениями) | §5.6 |
|
||
|
||
### 17.2. Открытые вопросы
|
||
|
||
| # | Вопрос | Предложение | Влияние |
|
||
|---|---|---|---|
|
||
| Q1 | Индикатор непрочитанных сообщений чата + sync между устройствами (backlog 23–24) | Поле вроде `Dialog.client_last_opened_at` / `last_read_message_id` + WS/REST counter; **не** тип уведомления `message` | Новая мини-постановка |
|
||
| Q2 | Точный HTTP-код send в уже `closed` dialog | Зафиксировать в OpenAPI (`409 resource_state_conflict` или `422`) | Клиентский UX |
|
||
| Q3 | Политика показа blocked-сообщения в ленте (полный текст vs redacted) | Минимизация PII в хранении blocked (M8 module-01) + нейтральный UI | DB + frontend |
|
||
| Q4 | Создание нового dialog сразу после `closed` — всегда разрешено или по бизнес-правилу «сессия поддержки» | MVP: разрешить, пока соблюдён unique active | Продукт / поддержка |
|
||
| Q5 | Stub `message-safety` с `400` на GET task vs канон arch-02 `403` | Для prod — только канон `200/203/403`; stub не расширяет публичный контракт | module-05 / contract tests |
|
||
| Q6 | Смешанный content text+files | Отдельный API version post-MVP | arch-02 breaking |
|
||
| Q7 | Unread badge на кнопке «Чат» vs бейдж колокольчика уведомлений | Развести визуально и в данных | UI + Q1 |
|
||
|
||
---
|
||
|
||
## 18. Связь со смежными доменами
|
||
|
||
| Домен | Связь |
|
||
|---|---|
|
||
| **Пользователь** | Чат только после JWT + bootstrap; отложенное сообщение после входа |
|
||
| **Уведомления** | CTA `send_chat_message` пишет в чат; кнопки Чат/Оператор на главной; тип `message` в уведомлениях **запрещён** |
|
||
| **Документы профиля** | Другой бакет/реестр; не заменяют вложения чата |
|
||
| **CRM (`bitrix-sync`)** | Contact map по телефону параллельно; не доставляет сообщения Open Lines |
|
||
|
||
Владелец ленты и статусов доставки — `api-backend` + App DB; рабочее место оператора — Bitrix24 Open Lines через `bitrix-local-app`.
|