Дизайн-демо к эпику batele-microservices#481. Модель v3 (скетч 10.07): чат — это
долгоживущий канал пары участников, не привязанный к заказу; контекст (заказ / доставка / тикет)
живёт на сообщении и работает как фильтр. Realtime: typing / read / presence / непрочитанное,
офлайн → push. Все пять поверхностей используют сквозную легенду: заказ #4821 ·
доставка D-7301 · ресторан «Тандыр» · клиентка Айгерим А. · курьерская служба
«Boomerang Go» · курьер Бакыт Т. · тикеты VS-102 / TS-88 / INC-311 —
одна и та же история видна глазами каждой роли.
chat/scopeRef → канал пары +
контекст на сообщении).Каждая карточка — самостоятельный прототип в дизайн-системе своего приложения. Мобильные демо
уже разобраны на задачи Flutter-командам: vendor → batele_vendor_flutter#6, курьер →
boomerang_go_flutter#1, b2c → boomerang-b2c#1.
Курьер · КурСлужба · Вендор · Клиент · SuperAdmin · SuperOrgcourierId / orgId / memberId / userIdsenderMemberIdid, chatId + опциональный контекст:orderId? (vendor order) + orderShortId?deliveryId? (go delivery)ticketId? (обращение/инцидент)filter: chatId); ② фильтр-режим — тот же канал, сужённый до одного заказа/доставки/тикета
(filter: chatId AND deliveryId): чип-фильтр в шапке «D-7301 ✕», композер автоматически тегирует
новые сообщения активным контекстом. Клик по чипу сообщения включает фильтр, «✕» снимает. Вход из карточки заказа/доставки открывает
всю историю канала (сообщения других заказов видны) — в шапке чип активного контекста, композер авто-тегирует
новые сообщения; фильтр «только этот заказ» — опциональный вид. Там же табы собеседников
«Клиент / Заведение / Курьер»: быстрое переключение вендор→курьер одним тапом; непрочитанное на табе — точка
(unread считается на канал).Пары остаются те же (каждая сторона видит свои 3 + поддержка): доступность пар с курьером/службой по-прежнему появляется после назначения доставки — но это влияет только на «когда можно написать первым в контексте», а не на существование канала.
| Пара (канал) | Когда доступна для первого сообщения | Типовой сценарий | Где в демо |
|---|---|---|---|
| Клиент ↔ Вендор | с момента создания заказа | «Можно без лука?», уточнения по составу и брони | c2c · vendor-web · vendor-mobile |
| Клиент ↔ Служба | после назначения доставки | вопросы по доставке в целом, до/вместо курьера | c2c · go-web |
| Клиент ↔ Курьер | после назначения курьера | «Я у подъезда, код 45#», фото передачи | c2c · courier-mobile |
| Вендор ↔ Служба | после назначения доставки | «Курьер задерживается?», готовность заказа | vendor-web · go-web |
| Вендор ↔ Курьер | после назначения курьера | «Какой из двух пакетов #4821?» | vendor-web · courier-mobile |
| Служба ↔ Курьер | после назначения курьера | эскалация: «клиент не отвечает 10 минут, что делать?» | go-web · courier-mobile |
Для вендора и курьерской службы участник чата — организация (actorId = orgId/deptId):
любой сотрудник с правом чата читает и пишет от её имени. В каждом сообщении фиксируется реальный автор —
senderMemberId. Клиент и курьер — индивидуальные акторы.
SuperAdmin.apps/auth/permissions.ts); на существующие роли
admin/manager не бэкфиллится → в демо есть заглушка «Недоступно для вашей роли».Тикет (support_ticket) = карточка со статусом/SLA/назначением; лента тикета —
это фильтр канала по ticketId: у пользователя один долгоживущий канал с поддержкой
(SuperAdmin↔Клиент, SuperAdmin↔Служба…), сообщения обращений TS-88 / TS-71 тегированы и фильтруются в нём.
Закрытие тикета не закрывает канал. Статусная модель:
open → pending →
resolved → closed.
| Контур | Агенты | Скоуп | Кто открывает | Консоль в демо |
|---|---|---|---|---|
| platform_incident Инциденты платформы | админы города / страны | маршрутизация по cityId; severity |
пользователи и авто-система (просрочка доставки, отмена после оплаты, спор оплаты) | root-admin (агент) · go-web (сторона) |
| platform_support Общая поддержка | админы платформы | любой пользователь платформы | клиент / курьер / сотрудник — из своего приложения | root-admin (агент) · c2c-mobile (клиент) |
| vendor_support Поддержка вендора | сотрудники вендора | строго в рамках orgId |
клиенты вендора | vendor-web / vendor-mobile (агент) · c2c (клиент) |
tempId, нет ack; при офлайне копится в очередиidchat:read (по lastReadMessageId)editedAt) · 🗑 удалено (deletedAt, «Сообщение удалено»)| Кейс | Поведение UI |
|---|---|
| Нет сети | баннер «Нет соединения — сообщения отправятся автоматически»; исходящие копятся в очереди с 🕓; после reconnect — авто-отправка по tempId (идемпотентно, без дублей) |
| Офлайн-участник | оркестратор уведомлений смотрит presence: онлайн → доставит realtime (push подавлен), офлайн → push «новое сообщение»; бейдж unread синхронизируется |
| Канал не материализован | виртуальная строка пары: пустое состояние + композер; первое сообщение идемпотентно создаёт канал (двойной тап ≠ два канала) |
| Сторона не назначена | в контексте заказа пара с курьером/службой показана с замком «после назначения курьера»; системное «Курьер назначен» появляется в канале с меткой контекста |
| Чип контекста | клик по чипу «#4821»/«D-7301»/«TS-88» на сообщении → включает фильтр-режим канала; «✕» на чипе в шапке — снимает |
| Сообщение без контекста | разрешено (обычная реплика в канале); в фильтр-режиме композер тегирует автоматически — «отправится с меткой D-7301» |
| Вложения | фото (грид), файл (чек PDF); загрузка через S3 presign; состояние «отправка…» на медиа; ограничения размера — TBD |
| Typing / read | «печатает…» — эфемерно, без персиста; read — разделитель «Непрочитанные» + ✓✓ у отправителя |
| Непрочитанное | бейджи на трёх уровнях: пункт меню/таб → тред в списке → разделитель в ленте; счётчик приходит с ListChats и обновляется по WS |
| Эскалация в поддержку | курьер → чат со своей службой; клиент → «Помощь» (platform_support); агент root — кнопка «Эскалировать в инцидент» на тикете; авто-инциденты по доменным событиям |
| Нет права | сотрудник без права «Чаты» видит заглушку с подсказкой попросить администратора роли |
| Заказ завершён | канал пары не закрывается — история остаётся, писать можно; завершение видно системным сообщением в контексте; ретеншн сообщений — открытый вопрос (TBD) |
chat:send {chatId|pair, body, tempId, ctx:{orderId?, deliveryId?, ticketId?}}WebSocket → apis/realtime-gateway; UI сразу рисует пузырь со статусом 🕓 по tempId; в фильтр-режиме ctx подставляется автоматически.
chat.CanAccess (с кешем) → chat.SendMessage; для виртуальной пары — идемпотентная материализация канала.
apps/chat персистит и публикуетСобытие chat.message.created в Redis (chatId + получатели).
Шлюз эмитит chat:message в chat:{chatId}; отправителю — ack с реальным id вместо tempId → статус ✓.
apps/notification слушает событие, сверяет presence в Redis, офлайн-участникам шлёт push «новое сообщение».
chat:read → chat.MarkRead → эмит остальным (✓✓); chat:typing — эфемерно, не персистится.
| Фаза | Что получает пользователь | UX-требования / риски |
|---|---|---|
| 0 · #475 apps/notification |
те же push/SMS, но из одного сервиса | поведение и тексты 1:1 с docs/NOTIFICATIONS.md; фундамент для «push только офлайн» и бейджа GetUnreadBadge |
| 1 · #480 realtime-gateway |
единое WS-подключение, стабильный reconnect | UX реконнекта: баннер офлайна, очередь отправки, ресинк непрочитанного после reconnect; presence с TTL+heartbeat — не «мигать» статусом при кратких обрывах |
| 2 · #476 ядро чатов |
первая переписка Клиент↔Вендор | статусы 🕓/✓/✓✓, unread-разделитель, подпись сотрудника у орг-стороны, вложения (если войдут в MVP), архив по завершении заказа |
| 3 · #477 каналы пар + контексты |
все пары у всех сторон, фильтры по контексту | виртуальные пары, замки до назначения, чипы контекста на сообщениях, фильтр-режим (chatId + orderId/deliveryId), сегмент собеседников в контексте заказа, идемпотентность первого сообщения |
| 4 · #478 apps/support |
тикеты и инциденты в трёх контурах | SLA-таймеры и просрочки, очередь «не назначено», «взять в работу», внутренние заметки vs ответ, эскалация тикета в инцидент, авто-инциденты с деталями события |
| 5 · #479 фронтенды |
пять поверхностей этого демо | сквозная согласованность: одна легенда, одинаковые статусы/бейджи/пустые состояния во всех приложениях; mobile-реализации по issue во Flutter-репо |
orderShortId + deliveryShortId (двойной чип возможен); vendor_support — в том же канале Клиент↔Вендор; Клиент↔Курьер пишет только при активной общей доставке; вход из заказа показывает всю историю канала; сквозная история видна сторонам целиком.support_ticket; фильтр-вид closed-тикета read-only, канал живёт.chat без scopeRef, ключ пары (уникальность по типам+id участников), chat_message.orderId/deliveryId/ticketId, gRPC: ListChats(actor), GetMessages(chatId, ctxFilter?, cursor), ListPairsForOrder(orderId, actor).