feat(tradein/support): веб-чат поддержки — серверная часть поверх Telegram-моста #2532
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2532
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/tradein-web-chat-backend"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Задача владельца продукта: клиент должен общаться с поддержкой в окне на сайте, не уходя в Telegram, при этом под капотом всё работает как сейчас — оператор отвечает в том же топике супергруппы и ничего нового не учит.
Раньше «вход в поддержку» был просто внешней ссылкой на
@MERAsupport_bot. Мост клиент↔оператор уже существовал (PR #2526), но только для тех, кто писал боту в личку.Как работает: клиент пишет из окна →
sendMessageв тот же топик с пометкой, что это с сайта и от какого пользователя → оператор отвечает реплаем как обычно → ответ доставляется в веб-тред. Маршрутизация по паре (support_chat_id,topic_message_id).Схема (
187_web_support_chat.sql): отдельные таблицыweb_support_threads/web_support_messages, а не колонкаchannelв существующихtg_support_*. Обоснование в комментарии миграции: у веб-клиента нет Telegramchat_id, который является первичным ключомtg_support_users— пришлось бы городить синтетический id или nullable-поля с XOR-семантикой в уже боевой таблице.Эндпоинты (
/api/v1/trade-in/support/*): отправить, получить сsince, счётчик непрочитанного, отметить прочитанным. Тред резолвится только поX-Authenticated-User—thread_idснаружи не принимается ни одной ручкой, чужой тред недостижим по любым параметрам.Правки по глубокому ревью
🟠 Заморозка всего API (было бы в проде). Ручка держала незакоммиченную запись и блокировку строки на время вызова Telegram. Бэкенд работает одним процессом с одним event loop (
uvicornбез--workers), сессия синхронная. Второй запрос того же пользователя (двойной клик, вторая вкладка) вставал бы на этой блокировке внутри синхронного вызова psycopg и останавливал event loop целиком — не чат, а весь tradein API, включая оценку и PDF. При лимите Telegram (штатные 30-60 с для группы, до 5 ретраев) это минуты. Причём сработало бы ровно при флуде, против которого и ставили ограничение частоты.Исправлено:
thread_idдо отправки не нужен вообще — создание треда перенесено после успешной отправки, окно блокировки исчезло. Плюс интерактивный путь получил свой бюджет ретраев (10 с, 1 попытка) вместо унаследованной воркерной политики.🟡 Тихая доставка ответа постороннему. Маршрутизация опиралась на негласное допущение «группа поддержки никогда не сменится»:
topic_message_idуникален в пределах чата, но между двумя таблицами constraint'а нет. При переезде в другую группу счётчик message_id начинается заново, и совпадение дало бы молчаливую отправку ответа чужому человеку в личку — инцидент по 152-ФЗ, который никак не детектируется.Исправлено, пока таблицы пустые: добавлена колонка
support_chat_idв обе таблицы (188_tg_support_chat_id_scope.sqlдля уже применённой 186), матч идёт по паре. При двойном совпадении — отказ в доставке сlogger.errorвместо тихого выбора.🟡 Медиа-ответ уходил в пустоту. Реплай картинкой на веб-зеркало писал WARNING в лог, оператор считал, что ответил. Фото с подписью доставляло подпись без картинки — тихая частичная доставка. Теперь отказ целиком с уведомлением в топик, по образцу уже существующей обработки 403.
Также: три ручки переведены с
async defнаdef(синхронные запросы к БД не должны исполняться в event loop),LIMITна выдачу треда, очистка корзин в ограничителе частоты, бюджет попыток не расходуется на неудачную отправку,.strip()на имени пользователя.Порядок выката
⚠️ Этот PR должен быть смержен и задеплоен РАНЬШЕ фронтового (
feat/tradein-web-chat-ui) — иначе окно чата откроется в 404.Бот на проде уже настроен и работает (контейнер
tradein-tgbotподнят 10 дней, токен и чат заданы в runtime-env).Test plan
uv run pytest -q— 2658 passed, 8 skippeduv run ruff checkс проектным конфигом — чисто