Skip to content

Chat Backend — Доработки для полноты функционала

Статус: все 5 доработок реализованы (2026-02-12)

Бэкенд чата работает, основа отличная. Ниже — список доработок, которые нужны фронтенду для полноценного UX. Каждый пункт — отдельная задача с примером ожидаемого результата.


1. Добавить image_url товара в Conversation

Проблема: Ответ GET /vendor/conversations и POST /vendor/conversations возвращает product: { id, title, price, status }нет фото товара. Без фото карточка диалога выглядит бедно. У Avito в каждом чате есть миниатюра товара.

Ожидаемый ответ:

json
{
  "data": {
    "id": 5,
    "product": {
      "id": 42,
      "title": "Тормозные колодки передние",
      "price": 2500,
      "status": "active",
      "image_url": "https://s3.partizap.ru/products/42/thumb_abc123.webp"
    },
    "companion": { ... },
    "last_message": { ... },
    "unread_count": 0
  }
}

Что нужно: при JOIN на products — взять primary image thumbnail (поле thumbnail_webp из product_images где is_primary = true). Если нет фото — null.

Приоритет: Высокий (влияет на весь UI списка чатов).


2. Добавить статус delivered для сообщений

Проблема: Сейчас статусы: sent → read. Нет промежуточного состояния «доставлено» (сообщение достигло устройства получателя, но ещё не прочитано).

Зачем: Стандарт мессенджеров (WhatsApp, Telegram, VK). Покупатель видит:

  • ✓ — отправлено (сервер принял)
  • ✓✓ — доставлено (дошло до получателя)
  • ✓✓ (синие) — прочитано

Без delivered покупатель не понимает: сообщение застряло или продавец просто не открыл чат?

Предложение по реализации:

Вариант A: Фронт подтверждает доставку (рекомендуемый)

  1. Фронт получателя получает message.new через Centrifugo
  2. Фронт отправляет POST /vendor/conversations/{id}/messages/{messageId}/delivered
  3. Бэкенд обновляет messages.status = 'delivered'
  4. Бэкенд публикует событие отправителю:
json
{
  "type": "message.delivered",
  "data": {
    "conversation_id": 123,
    "message_id": 456
  }
}

Вариант B: Batch delivery confirmation

Чтобы не слать по запросу на каждое сообщение — фронт подтверждает последний полученный message_id:

POST /vendor/conversations/{id}/delivered
{ "last_message_id": 456 }

Бэкенд обновляет все sent сообщения до этого ID → delivered.

Итоговая цепочка статусов: sent → delivered → read

Обновить Zod-тип: messageStatusSchema = z.enum(['sent', 'delivered', 'read'])

Приоритет: Средний (UX улучшение, но не блокирует базовый функционал).


3. Добавить тип сообщений system

Проблема: Сейчас типы: text | image. Нет системных сообщений.

Зачем: Покупатель ведёт переговоры, а товар уже продан. Без системного сообщения в чате он об этом не узнает.

Сценарии:

  • Товар продан → "Товар «Фара левая BMW E46» отмечен как проданный"
  • Цена изменена → "Продавец изменил цену: 5 000 ₽ → 4 500 ₽"
  • Товар удалён → "Объявление удалено продавцом"
  • Товар снова активен → "Объявление снова доступно"

Ожидаемое сообщение:

json
{
  "id": 789,
  "conversation_id": 123,
  "sender_id": null,
  "type": "system",
  "text": "Товар «Фара левая BMW E46» отмечен как проданный",
  "image_url": null,
  "image_thumbnail": null,
  "status": "sent",
  "created_at": "2026-02-11T20:00:00+00:00"
}

Что нужно:

  1. Добавить 'system' в enum типов сообщений
  2. Разрешить sender_id: null для системных сообщений
  3. При смене статуса товара (sold, archived, deleted, reactivated) — создать системное сообщение во всех активных conversations по этому товару
  4. Публиковать message.new обоим участникам через Centrifugo

Триггеры на бэке:

  • PUT /vendor/products/{id} (если меняется price) → system message
  • POST /vendor/products/{id}/publish → нет (не нужно)
  • Товар переходит в sold / archived / rejected → system message
  • DELETE /vendor/products/{id} → system message

Приоритет: Средний (важно для UX, но не блокирует запуск чата).


4. Добавить last_read_message_id в событие message.read

Проблема: Сейчас событие:

json
{
  "type": "message.read",
  "data": {
    "conversation_id": 123,
    "reader_id": 7
  }
}

Нет информации о том, до какого сообщения прочитано. Фронт вынужден пометить ВСЕ свои сообщения как read, даже если новые пришли после момента прочтения.

Ожидаемое событие:

json
{
  "type": "message.read",
  "data": {
    "conversation_id": 123,
    "reader_id": 7,
    "last_read_message_id": 456
  }
}

Фронт пометит read только сообщения с id <= 456. Сообщения отправленные позже останутся sent/delivered.

Что нужно: при POST /conversations/{id}/read — запомнить ID последнего сообщения собеседника и включить его в Centrifugo event.

Приоритет: Низкий (текущий вариант работает для 90% случаев, но edge cases возможны).


5. Realtime online-статус через Centrifugo

Проблема: Сейчас GET /vendor/users/{id}/online — REST запрос с 5-минутным окном. Проблемы:

  • Фронт видит статус только при открытии чата (одноразовый GET)
  • Нет realtime обновления: не увидим как собеседник «появился в сети» пока мы в чате
  • Для обновления нужен polling каждые N секунд → лишние запросы к PHP

Предложение: при изменении online-статуса (подключение/отключение Centrifugo) — публиковать событие в персональные каналы собеседников всех активных диалогов.

Ожидаемое событие:

json
{
  "type": "presence",
  "data": {
    "user_id": 7,
    "online": true,
    "last_seen_at": null
  }
}
json
{
  "type": "presence",
  "data": {
    "user_id": 7,
    "online": false,
    "last_seen_at": "2026-02-11T20:15:00+00:00"
  }
}

Как реализовать:

Centrifugo поддерживает connect/disconnect proxy. При подключении/отключении клиента Centrifugo может вызывать PHP webhook:

POST /api/centrifugo/connect    → уже есть (валидация сессии)
POST /api/centrifugo/disconnect → добавить

В disconnect handler:

  1. Обновить last_seen_at в Redis/БД
  2. Найти все активные conversations пользователя
  3. Опубликовать presence { online: false } в personal:#companionId для каждого собеседника

В connect handler (существующий):

  1. После успешной валидации — опубликовать presence { online: true } собеседникам

Альтернатива (проще): оставить REST endpoint, но фронт будет polling раз в 30с пока чат открыт. Менее элегантно, но рабочий вариант.

Приоритет: Низкий (REST + polling работает, realtime через Centrifugo — nice-to-have).


Сводная таблица

#ДоработкаПриоритетСтатус
1product.image_url в conversationsВысокийГотово
2Статус deliveredСреднийГотово
3Тип system сообщенийСреднийГотово
4last_read_message_id в message.readНизкийГотово
5Realtime presence через CentrifugoНизкийГотово

Что работает и не требует изменений

  • Все REST endpoints (conversations, messages, read, typing, unread-count)
  • Connect Proxy (авторизация через куки)
  • Centrifugo events (message.new, typing, message.read, unread.update)
  • Image upload с EXIF stripping и thumbnails
  • Cursor pagination
  • Soft delete с автовосстановлением
  • Online middleware (Redis TTL 5 мин)
  • Rate limiting