Заказ ещё едет по городу в курьерской сумке, а бот уже интересуется: «Как вам покупка?». Клиент смотрит на телефон, потом на пустую прихожую и мысленно ставит единицу. Не потому что товар плох, а потому что бот оказался слишком проворным. Обычно причина проста: таймер запустили от неправильного события. Есть дата создания заказа, есть дата оплаты, есть дата отгрузки, и каждая из них создаёт иллюзию, что «прошло достаточно времени». Но ни одна из них не означает, что человек получил то, о чём его теперь спрашивают.
Эта статья про то, как собрать бота обратной связи, который просит отзыв в подходящий момент, не пишет по ночам, не спрашивает про отменённый заказ и замолкает, как только клиент сказал «хватит». Мы пройдём путь от выбора события до связывания ответа с заказом и посмотрим, какие решения принимает расписание, а какие обязан принимать ваш код.
Какое событие считать концом заказа
Начнём с вопроса, который кажется скучным: от чего отсчитывать время. Варианты бывают такие.
- Создание заказа. Самая удобная дата, потому что она есть всегда. И самая бесполезная: между созданием и получением могут пройти минуты (цифровой товар) или две недели (заказ под изготовление).
- Оплата. Лучше, но по-прежнему говорит о деньгах, а не о результате. Оплаченный заказ можно отменить, потерять на складе или вернуть.
- Передача в доставку. Товар в пути, оценивать нечего.
- Подтверждённое завершение. Курьер отметил вручение, клиент получил код, пункт выдачи закрыл заказ, услуга оказана и акт подписан. Только эта точка отвечает на вопрос «человеку уже есть что оценивать».
Поэтому в основе бота должно лежать событие вида «заказ выполнен», которое приходит из вашей системы учёта: CRM, магазина, складской программы. Статус нужно брать из источника, который действительно знает о вручении, а не угадывать по прошедшему времени. Если такого источника нет и статус ставит менеджер руками, это нормально, но тогда ваш бот честно зависит от дисциплины менеджера, и об этом стоит знать заранее.
Полезно записать событие вместе с моментом, когда оно произошло, а не когда дошло до бота. Интеграции задерживаются, повторяются, приходят пачкой после сбоя. Если считать «через два часа после получения уведомления», то после ночного простоя бот отправит весь накопившийся хвост одновременно. Если считать «через два часа после фактического вручения», расписание останется осмысленным и после задержек.
Согласие и возможность написать — разные вещи
Теперь второй поворот. В мессенджере у бота технически может быть возможность написать пользователю: человек когда-то нажал «Старт», бот знает его идентификатор чата. Но из этого не следует, что клиент согласился получать просьбы об отзыве. Это две разные вещи.
- Техническая возможность — идентификатор чата и отсутствие блокировки бота. Её нельзя считать гарантией: если человек заблокировал бота, сообщение просто не дойдёт, и никакие настройки webhook этого не изменят.
- Согласие — осознанное решение клиента, что после заказа ему можно писать с просьбой оценить сервис. Это процесс вашего приложения: рассылки и согласие не появляются сами собой от того, что вы подключили webhook.
На практике это означает отдельное поле в вашей базе. Не «есть chat_id», а, например, «статус запросов обратной связи»: разрешено, отказ, неизвестно. Момент получения согласия лучше сопоставить с понятным действием: кнопка при оформлении, ответ на первое сообщение бота, галочка в личном кабинете. Если клиент пришёл из чата поддержки по конкретной проблеме, это ещё не повод присылать ему опрос через три дня.
Заведите правило: сообщение отправляется, только если одновременно выполнены три условия — известен чат, согласие не отозвано, статус запросов не равен «отказ». Любое из них ложное, и задание тихо завершается без отправки. Тихо — важное слово: пропуск не должен превращаться в ошибку, которая повторяется по кругу.
Когда писать: время, часовой пояс и окно контакта
Допустим, заказ вручён в 22:40. Если задержка «два часа» — отзыв придёт в 00:40. Формально всё по правилам, фактически вы разбудили человека вопросом про качество упаковки. Поэтому у расписания два слоя: задержка после события и допустимое окно, в которое вообще можно писать.
Условный пример. Пусть задержка составляет три часа, а окно контакта — с 10:00 до 20:00 по местному времени клиента.
| Вручение | Через 3 часа | Попадание в окно | Отправка |
|---|---|---|---|
| 11:15 | 14:15 | да | 14:15 в тот же день |
| 18:30 | 21:30 | нет, позже 20:00 | 10:00 следующего дня |
| 07:10 | 10:10 | да | 10:10 в тот же день |
| 23:05 | 02:05 | нет, раньше 10:00 | 10:00 следующего дня |
Правило простое: считаем момент «событие плюс задержка»; если он вне окна, переносим на ближайшее открытие окна. Так бот не нарушает тишину, и при этом каждое сообщение остаётся достаточно близким к получению заказа.
Самое коварное здесь — часовой пояс. Если у вас клиенты из разных регионов, «десять утра» без уточнения, чьё это десять утра, превращается в лотерею. Нужно хранить пояс клиента (по адресу доставки или по настройке профиля) и выполнять сравнение с окном именно в нём. Если пояс неизвестен, выберите осторожное поведение: например, пояс магазина и более узкое окно в середине дня.
Если планировщиком у вас служит n8n, учтите документированную особенность узла Schedule Trigger: расписание работает только у опубликованного workflow и зависит от часового пояса самого workflow либо экземпляра (документация n8n). Практический вывод: пояс в настройках workflow стоит проверить явно, а не надеяться на то, что сервер «настроен как нужно». Сдвиг на несколько часов легко превращает окно 10–20 в ночное. И ещё одно следствие: этот пояс относится к самому расписанию, а не к вашему клиенту. Для клиентского времени считать нужно отдельно, в логике проверки.
Два способа поставить сообщение в очередь
Есть два подхода. Первый: при событии «заказ выполнен» создать в своей базе запись «отправить отзыв в такое-то время», а расписание раз в несколько минут просматривает записи, у которых время наступило. Второй: на каждое событие создавать отдельный отложенный таймер. Для бота обратной связи первый обычно проще и понятнее. Запись видна, её можно отменить, поменять время, посмотреть статус. Таймер же живёт где-то внутри инструмента, и отменить его из другого сценария бывает неудобно.
Минимальный набор полей для такой записи: идентификатор заказа, идентификатор клиента, плановое время, статус (ожидает, отправлено, пропущено, отменено), причина пропуска и время последней попытки. Причина пропуска пригодится позже, когда кто-то спросит: «А почему этому клиенту не пришёл опрос?»
Расписание не знает, что произошло с заказом
Здесь главный поворот всей схемы. Расписание умеет одно: в нужное время запустить сценарий. Оно не знает, что за те три часа ожидания заказ вернули, что клиент написал в поддержку с претензией или что он нажал «не присылайте мне опросы». Состояние заказа и клиента живёт в ваших данных, и проверять его нужно в момент отправки, а не в момент постановки задания.
Поэтому последовательность выглядит так.
- Доставлен. Из учётной системы приходит событие о подтверждённом вручении. Создаётся запись задания со статусом «ожидает» и плановым временем по правилу задержки и окна.
- Ожидание. Ничего не происходит. Задание лежит в базе, расписание периодически просматривает те, у которых время наступило.
- Проверка. Непосредственно перед отправкой сценарий заново читает актуальные данные: не отменён ли заказ, нет ли возврата, нет ли открытой претензии, не стоит ли у клиента отказ, жив ли чат. Если что-то из этого не так, задание получает статус «пропущено» с причиной.
- Отправка. Если проверка пройдена, бот отправляет сообщение, а запись переводится в статус «отправлено» с отметкой времени.
И ветка отказа. Клиент отвечает на сообщение «Больше не спрашивать» или нажимает соответствующую кнопку. Тогда:
- в профиле клиента статус запросов меняется на «отказ» с датой;
- все его ожидающие задания получают статус «отменено» с причиной «отказ клиента»;
- бот отвечает одной короткой фразой с подтверждением и больше ничего не добавляет;
- новые задания для этого клиента не создаются, а если создаются по ошибке, то пропускаются на шаге проверки.
Обратите внимание на двойную защиту. Отмена ожидающих заданий — это удобство и чистота данных. Но настоящую гарантию даёт проверка перед отправкой: даже если отмена по какой-то причине не сработала или задание создала другая часть системы, на последнем шаге отказ всё равно будет замечен. Такая защита дешёвая: один запрос в базу на каждое отправляемое сообщение.
Фраза подтверждения тоже имеет значение. Не стоит просить «а может, всё-таки оставите отзыв?» и тем более уточнять причины отказа. Достаточно: «Хорошо, больше не будем спрашивать про заказы. Если передумаете, напишите в любой момент». Это одно сообщение, и после него бот молчит о запросах отзывов.
Как понимать «понятный отказ»
Кнопка надёжнее текста. Если в каждом запросе есть кнопка «Не присылать больше», нажатие однозначно. Свободный текст вроде «отстаньте» или «не надо» тоже стоит распознавать, но здесь лучше перестраховаться: при сомнении не продолжать запросы, а один раз уточнить, и то только если сообщение действительно неоднозначное. Для простого бота хватит списка явных фраз плюс кнопки, а всё остальное отправлять в обычный диалог с поддержкой.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Повторная доставка события: как не создать два задания
Переходим к самой технической части. События от внешних систем приходят не по одному разу. Если бот принимает обновления через webhook Telegram, действует правило: при неуспешном HTTP-ответе Telegram повторяет доставку (документация Bot API, setWebhook). Отсюда прямое следствие: если ваш обработчик ответил ошибкой уже после того, как создал запись, повтор создаст вторую. А если вы создаёте задание «по событию о выполнении заказа» из своей учётной системы, у той тоже могут быть свои повторы, и логика та же.
Решение не в том, чтобы «ответить побыстрее», а в том, чтобы обработка была безопасна к повтору. Это делается через естественный ключ. Для задания обратной связи таким ключом служит идентификатор заказа плюс тип запроса: на один выполненный заказ допускается ровно одно задание на отзыв. В базе это уникальное ограничение. Второе событие с тем же ключом либо не создаёт запись, либо обновляет существующую, не меняя уже отправленный статус.
Важный нюанс: новый случайный идентификатор попытки сам по себе ничего не защищает. Если каждое повторение события получает свежий идентификатор, база честно создаст две записи. Ключ должен быть привязан к смыслу события (этот заказ, этот запрос), а не к самой попытке доставки.
Второй нюанс касается внешних вызовов. Нельзя рассчитывать, что запись в вашу базу и отправка сообщения в мессенджер происходят как одна неделимая операция: между ними всегда есть щель. Поэтому порядок такой. Сначала задание переводится в состояние «отправляется» условным обновлением (только если сейчас оно «ожидает»). Если обновление затронуло строку, сценарий продолжает и отправляет сообщение. Если не затронуло, значит, кто-то уже взял это задание, и сценарий выходит. После успешной отправки статус становится «отправлено». При сбое отправки задание возвращается в «ожидает» с увеличенным счётчиком попыток и новым временем, а после нескольких неудач получает статус «ошибка» для ручного разбора. Такая схема не дублирует сообщения при параллельных запусках, а при аварийной остановке посреди отправки оставляет заметный след: задание зависло в «отправляется», и его можно разобрать вручную, выбрав, повторять или нет.
Если вы принимаете обновления Telegram через webhook, стоит использовать и секретный токен: он передаётся в специальном заголовке запроса, и обработчик может сверять его, отсекая чужие обращения (там же). Это защита входа, она не заменяет проверку согласия и статуса заказа, но не даёт посторонним подсунуть боту поддельное событие, которое запустит опрос.
Привязка ответа к заказу
Теперь представьте, что клиент получил вопрос и ответил: поставил оценку и написал пару строк. Бот видит обычное входящее сообщение. К какому заказу оно относится? Если у клиента был один заказ, догадаться легко. Если три заказа за неделю, ошибиться просто, и отзыв о кофе окажется под заказом с чайником.
Надёжнее не угадывать, а передавать контекст. В запрос на отзыв встраивается кнопка, чьи данные содержат идентификатор задания или заказа. Когда клиент нажимает «5» или «2», приходящее нажатие уже несёт ссылку на нужный заказ. Ограничение по размеру данных кнопки учтите заранее: кладите короткий идентификатор задания, а не всю структуру. Сопоставление «задание → заказ» делается на вашей стороне.
Для текстового комментария после оценки пригодится простая сессия: после нажатия кнопки в профиле клиента запоминается «ожидается комментарий к заказу N». Следующее текстовое сообщение, пришедшее в разумный срок, привязывается к этому заказу, а сессия закрывается. Если сообщение пришло через сутки, лучше не привязывать его автоматически, а передать в обычную поддержку. Свободный текст без контекста чаще всего окажется новым вопросом, а не продолжением отзыва.
Повторное нажатие кнопки с оценкой тоже должно быть безопасным. Клиент мог нажать дважды или передумать. Правило: на один заказ хранится один отзыв, повтор обновляет оценку, а не создаёт новую запись. Так же, как и с заданиями, смысловой ключ — заказ — защищает от дублей лучше любых счётчиков.
Что хранить вместе с отзывом
- идентификатор заказа и клиента;
- оценку и текст комментария, если он есть;
- время получения и ссылку на задание, из которого пришёл запрос;
- признак того, что клиент отказался от дальнейших запросов, если он сделал это в том же диалоге.
Последний пункт полезен потому, что люди часто отвечают одним сообщением: «Всё хорошо, но больше не пишите». Отзыв сохраняется, отказ фиксируется отдельно, и оба факта не мешают друг другу.
Где всё это запускать
Для такой связки нужно немного: постоянно работающий обработчик webhook, база для заданий и клиентов, планировщик. Если вы используете n8n, расписание в нём будет работать только у опубликованного workflow, поэтому сервис должен быть доступен постоянно, а не только пока открыт редактор. Для этого подходит собственный сервер: на VPS MAATRIX есть NVMe-диски, процессоры AMD EPYC и root-доступ, а в публичном каталоге предложены n8n и Nextcloud с установкой (точное наличие и операционную систему лучше проверить в форме заказа). В предложении указаны локации UK, US, RU и FR, оплата картой РФ и через СБП, а VPS создаётся автоматически через пару минут после оплаты.
По ресурсам такой бот нетребователен: он в основном ждёт событий и выполняет короткие запросы. Для небольшого магазина на старте может хватить младшего тарифа Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe), но это ориентир, а не обещание ёмкости: реальную нагрузку покажут ваши объёмы заказов, размер базы и то, что ещё работает на той же машине. Если рядом поселятся другие сервисы, имеет смысл посмотреть на Uab (2 vCPU, 4 ГБ, 80 ГБ) или выше и замерить использование памяти на своём сценарии. Резервные копии базы заданий и отказов стоит настроить самостоятельно: автоматические копии во всех тарифах в публичных фактах не заявлены, а потеря списка отказов означает, что бот начнёт писать тем, кто просил этого не делать.
О том, как пересылать служебные сообщения на ваш собственный чат (например, когда задания падают в «ошибку»), мы писали отдельно в материале про бота для уведомлений о событиях на сервере. Эти оповещения полезно держать отдельно от клиентских сообщений: у них другая аудитория и другие правила.
Один заказ от начала до тишины
Пройдём всю цепочку на условном примере. Клиентка оформила заказ в понедельник, курьер вручил его во вторник в 18:30, учётная система отправила событие «выполнен». Бот создаёт задание с ключом «заказ + запрос отзыва»; три часа спустя получается 21:30, это вне окна 10:00–20:00 по её времени, значит, плановая отправка переносится на среду, 10:00. Повторная доставка того же события через минуту упирается в уникальный ключ и ничего не создаёт.
В среду в 10:00 расписание подхватывает задание. Перед отправкой сценарий проверяет данные: заказ не отменён, возврата нет, отказа клиентки нет, согласие на запросы есть. Задание переводится в «отправляется», бот присылает короткое сообщение с кнопками оценки и ссылкой «Больше не присылать». Клиентка нажимает «4», бот сохраняет оценку под нужным заказом, предлагает написать комментарий и запоминает, что следующий текст относится к этому заказу.
Она пишет: «Всё доехало целым, спасибо. Но опросы мне больше не присылайте». Комментарий сохраняется в отзыве, в профиле появляется статус «отказ» с датой, все ожидающие задания по её заказам отменяются, а бот отвечает единственной фразой с подтверждением. Через неделю она оформляет новый заказ, и когда он будет вручён, система создаст задание, как и для всех. Но на шаге проверки сценарий увидит отказ, запишет причину пропуска и завершится без единого сообщения. Бот молчит, а четвёрка по-прежнему лежит под тем заказом, о котором шла речь.