Практика: каталог, списки и заявки
Шесть упражнений: поиск занятия, связанные записи, обработка HTTP-списка, итоги, подтверждение заявки, команды в диалоге и составление пар. С настройками и ожидаемыми результатами.
Sur cette page
В этих упражнениях вы соберёте каталог занятий и подтверждение заявки. Они показывают, как соединить новые действия реакций: получить данные, проверить результат, показать выбор и сохранить изменение.
Работайте в учебном проекте. Первые четыре урока используют свои коллекции и не требуют платного внешнего сервиса. Во втором уроке для получения списка понадобится HTTP-источник; саму обработку можно сначала проверить на приведённом JSON в редакторе.
| Что хотите сделать | С какого урока начать |
|---|---|
| Найти занятие по названию и предложить варианты | Урок 1: record_search, branch_result, record_related |
| Отобрать и отсортировать список из HTTP-ответа | Урок 2: list_transform |
| Посчитать число занятий и стоимость по категориям | Урок 3: list_aggregate |
| Подтвердить заявку, только пока она новая | Урок 4: record_update_if, branch_value |
| Дать человеку выйти из анкеты командой | Урок 5: escape_on_command и проверка кнопок |
| Разделить участников на пары | Урок 6: формула PAIRUP |
В таблицах ниже куда сохранить означает имя временного результата текущего запуска, например search. В следующем действии пишите это имя без ctx. и без скобок. В тексте сообщения используйте подстановку, например {{ctx.search.first.name}}. Постоянные данные пользователя сохраняются отдельно в параметрах или коллекциях.
Урок 1. Найти занятие и показать варианты
Результат: человек пишет «акварель», бот находит занятие, предлагает дату и после нажатия читает актуальную запись.
Подготовьте данные
В разделе Данные создайте коллекцию classes. Поля code, name, aliases имеют тип Текст.
| code | name | aliases |
|---|---|---|
| wc | Акварель | Рисование водой |
| clay | Лепка | Керамика |
Создайте вторую коллекцию sessions: class_code, title, state типа Текст, price типа Число.
| class_code | title | state | price |
|---|---|---|---|
| wc | Акварель в субботу | open | 1500 |
| wc | Акварель в воскресенье | open | 1800 |
| clay | Лепка в субботу | open | 2000 |
code=wc – ваш постоянный код занятия. _record_id – внутренний ID конкретной строки коллекции. В этом уроке коллекции связаны через code и class_code; для выбора конкретной даты используем _record_id строки sessions.
Настройте поиск и выбор ветки
Создайте реакцию для текстового запроса. Если в проекте уже есть общий ответ на любой текст, разместите учебную реакцию выше него или ограничьте её условиями.
Добавьте Поиск по коллекции (record_search):
| Настройка | Значение |
|---|---|
| Коллекция | classes |
| Запрос | {{text}} |
| Поля поиска | name |
| Поля с альтернативными названиями | aliases |
| Режим | auto |
| Учитывать опечатки | Включено |
| Максимум строк | 20 |
| Куда сохранить | search |
Следующим действием добавьте Выбрать следующий шаг (branch_result). Источник – search. Ветки проверяются сверху вниз; сработает первая подходящая.
Сначала создайте целевые реакции с ответами из таблицы и включите их. Если они нужны только для переходов, не задавайте им самостоятельный текстовый триггер или команду. Затем выберите их по названиям в ветках.
| Порядок | Условие | Что делает целевая реакция |
|---|---|---|
| 1 | Неполная выдача (truncated) | Просит уточнить запрос |
| 2 | Ничего не найдено (empty) | Предлагает другое название |
| 3 | Нечёткое совпадение (fuzzy) | Показывает «Вы имели в виду…?» и просит подтвердить выбор |
| 4 | Единственный результат (single) | Получает даты найденного занятия |
| 5 | Несколько результатов (multiple) | Показывает найденные занятия кнопками |
| Иначе | Запасной переход | Просит уточнить запрос |
Нечёткое совпадение ставим раньше единственного результата: одна строка с похожим названием ещё не означает, что пользователь выбрал именно её. Точное совпадение тоже можно выделить отдельной веткой, но оно само по себе не означает единственность записи.
Для нескольких занятий используйте помощник Кнопки из результата: источник search, подпись по name, ключ выбора по code. Каждая кнопка должна передавать selected_code своего занятия. Для единственного результата передайте в следующее действие {{ctx.search.first.code}}.
В реакции получения дат добавьте Найти связанные записи (record_related): коллекция sessions, поле связи class_code, значение выбранного code, дополнительный фильтр state=open, результат sessions_result. Для ветки с кнопками значение берётся из {{ctx.selected_code}}.
После чтения снова используйте branch_result: неполная выдача, пустой результат, одна дата, несколько дат. Из sessions_result создайте кнопки с названием title. Передавайте selected_id из _record_id.
Обработчик даты заново читает коллекцию sessions через Найти связанные записи: поле _record_id, значение {{ctx.selected_id}}, результат selected_session. Лишь после проверки результата покажите {{ctx.selected_session.first.title}} и {{ctx.selected_session.first.price}}.
Проверьте сценарий
Акварельдолжно привести к двум датам.Рисование водойдолжно найти то же занятие по альтернативному названию.Акварелдолжно предложить подтвердить похожее название; не оформляйте заявку сразу по опечатке.Астрономиядолжно привести к сообщению о пустом результате.- Для проверки неполной выдачи временно поставьте максимум связанных строк
1: две даты не должны превратиться в «единственную доступную дату». - Отправьте каталог, поменяйте цену даты в коллекции и нажмите старую кнопку. Карточка должна показать свежую цену. Если строку удалили, обработайте пустой результат вместо показа старых данных.
Если не получается: проверьте, что имена коллекций и полей совпадают; в связанных записях сравниваются одинаковые коды. В Проверить поиск вводите конкретный текст, а не {{text}}: предварительная проверка читает данные, но не запускает следующую реакцию.
Подробности: коллекции, динамические кнопки и Carry.
Урок 2. Оставить доступные занятия и убрать повторы
Результат: из пяти строк получатся два самых дешёвых доступных занятия без повторов. При этом бот сохранит информацию, что есть ещё один результат.
Пусть HTTP-сервис возвращает объект с полем items:
{
"items": [
{"id":"c1","category":"watercolor","title":"Акварель","state":"open","price":1500},
{"id":"c2","category":"clay","title":"Лепка","state":"open","price":2000},
{"id":"c1","category":"watercolor","title":"Акварель","state":"open","price":1500},
{"id":"c3","category":"sketch","title":"Скетчинг","state":"closed","price":900},
{"id":"c4","category":"watercolor","title":"Акварель вечером","state":"open","price":1800}
]
}
- В действии Запрос к источнику выберите JSONPath
itemsи сохраните результат под именемcatalog. Нужен массив из пяти объектов, а не весь объект ответа. - Добавьте Обработать список (
list_transform). - Укажите настройки ниже и сохраните результат как
selection. - Для кнопок используйте
selection.rows. Поле выбора здесьid: это обычные данные внешнего сервиса, у них нет обязательного_record_idколлекции.
{
"source": "catalog",
"filters": [{"field":"state","operator":"eq","value":"open"}],
"sort": [{"field":"price","direction":"asc","mode":"number"}],
"distinct_by": ["id"],
"offset": 0,
"limit": 2,
"save_to_ctx": "selection"
}
В редакторе эти параметры задаются отдельными полями. Для предварительной проверки вставьте сам массив items, без внешнего объекта. Проверка не отправляет сообщения и не меняет записи.
Ожидается: selection.rows содержит c1 за 1500 и c4 за 1800, в этом порядке. count=2, total=3, truncated=true: три занятия прошли отбор, но показаны два. Повтор c1 исчез, закрытый c3 отфильтрован.
Чтобы показать оставшуюся строку в том же исходном списке, поставьте offset=2: останется c2. Для нового пользовательского запроса сначала снова получите данные. Само окно offset/limit не хранит состояние пагинации и не создаёт кнопку «Далее».
Если не получается: сортировка 10, 2 вместо 2, 10 означает, что выбран текстовый режим. Ошибка источника часто означает, что в catalog попал весь объект с items. Неверное число вроде "цена неизвестна" не превращается в ноль: исправьте данные или отберите такие строки до числовой операции.
Как показать полученные строки кнопками: динамические кнопки и передача параметров.
Урок 3. Посчитать итоги по категориям
Результат: бот посчитает количество и стоимость доступных занятий по каждой категории.
- Возьмите
catalogиз прошлого урока. - Повторите обработку списка с фильтром
state=openи удалением повторов поid, но поставьтеlimit=100и сохраните результат какavailable_classes. Для нашего примера это все три подходящие строки. - Добавьте Группы и итоги (
list_aggregate). Источникavailable_classes, группировка поcategory, результатtotals. - Добавьте три метрики:
class_countс операциейcountбез поля;price_sumсsumпоprice;price_avgсavgпоprice.
{
"source": "available_classes",
"group_by": ["category"],
"metrics": [
{"as":"class_count","operator":"count"},
{"as":"price_sum","operator":"sum","field":"price"},
{"as":"price_avg","operator":"avg","field":"price"}
],
"save_to_ctx": "totals"
}
Ожидается: в totals.rows две группы.
| category | class_count | price_sum | price_avg |
|---|---|---|---|
| watercolor | 2 | 3300 | 1650 |
| clay | 1 | 2000 | 2000 |
Уберите группировку и выполните действие заново: общий итог должен содержать class_count=3 и price_sum=5300. totals.count при этом равно 1, потому что в результате одна строка итога. Число занятий берётся из вашей метрики totals.first.class_count, а не из числа строк результата.
Для сообщения используйте {{ctx.totals.first.class_count}} и {{ctx.totals.first.price_sum}}. Для нескольких групп можно пройти по totals.rows в for_each.
Проверьте ошибочный вариант: передайте в подсчёт selection с лимитом 2 из предыдущего урока. Сумма будет 3300 только по видимым строкам, а не 5300. Полнота исходного результата не восстанавливается после подсчёта. Надпись «Итого по всей базе» здесь будет неверной.
Для пустого списка без группировки count и sum дают ноль, avg, min, max – null. Не показывайте null пользователю как среднюю цену: добавьте отдельный ответ для пустого набора.
Урок 4. Подтвердить заявку один раз
Результат: первый запуск подтвердит новую заявку, повторный получит конфликт и не покажет сообщение об успешном подтверждении.
Создайте коллекцию practice_bookings: поля guest_id и state типа Текст. Добавьте учебную строку с вашим user_id и state=new. Передайте её внутренний _record_id в selected_id кнопки. Это должен быть ID строки коллекции, а не код занятия wc.
В обработчике кнопки добавьте Изменить запись при условии (record_update_if):
{
"collection": "practice_bookings",
"record_id": "{{ctx.selected_id}}",
"expected": {"state":"new","guest_id":"{{user_id}}"},
"values": {"state":"confirmed"},
"save_to_ctx": "transition"
}
Проверка владельца и статуса выполняется вместе с изменением записи. Если два запуска одновременно увидели новую заявку, только один сможет изменить её из new в confirmed.
Далее добавьте Выбрать шаг по значению (branch_value). Значение – {{ctx.transition.status}}. Создайте отдельные целевые реакции и выберите их в редакторе:
| Условие | Ответ целевой реакции |
|---|---|
Равно updated | «Заявка подтверждена» |
Равно conflict | «Заявка уже изменилась. Откройте её заново» |
Равно not_found | «Заявка больше недоступна» |
| Иначе | «Не удалось подтвердить заявку. Попробуйте открыть её заново» |
Сообщение об успехе должно находиться только в реакции updated. Условное изменение возвращает статус, а не автоматически выбирает ответ. При конфликте не добавляйте безусловное обновление: оно отменит защиту.
Эту же развилку можно настроить прямо в исходах условного изменения: updated, conflict, not_found, error. Тогда отдельное действие branch_value не понадобится. Выбранный исход завершает текущую цепочку; сообщение об успехе по-прежнему остаётся только в реакции updated. Пошаговый пример с исходами.
Для проверки временно оставьте учебную кнопку многоразовой и выполните два нажатия. Сначала ожидается updated, затем conflict. Удалите строку и нажмите кнопку ещё раз: ожидается not_found. Отдельно проверьте строку с другим guest_id: изменения не должны сохраниться, результат будет conflict.
Одноразовая кнопка ограничивает использование сохранённых данных кнопки. Проверка состояния защищает саму запись, в том числе при двух разных кнопках или одновременных запусках. Для внешней оплаты или выдачи услуги дополнительно нужен способ не повторять одну операцию в том внешнем сервисе: изменение строки не делает всю цепочку неделимой.
Подробнее о выборе и изменении записи: работа с коллекциями. Условное изменение добавляет к фильтру проверку текущего состояния.
Урок 5. Проверить команды и старые кнопки
Результат: человек сможет выйти из вопроса по /start, если вы явно разрешили это в настройках.
- Создайте учебную реакцию с вопросом «Как вас зовут?» и включённым ожиданием текстового ответа.
- Проверьте, что отдельная активная реакция отвечает на
/start. - Пока Команды бота прерывают ожидание выключено, отправьте
/startв ответ на вопрос. Это остаётся входным ответом и проходит проверку шага. Например, шаг без проверки может сохранить строку/startкак имя. - Включите Команды бота (например, /start) прерывают ожидание (
escape_on_command) и сохраните реакцию. - Снова запустите вопрос и отправьте
/start. Ожидание должно сняться, после чего команда обрабатывается обычными реакциями. Если реакции на неё нет, сама настройка не создаст приветствие. - Отдельно проверьте
/cancelили «отмена»: они снимают ожидание и используют настроенную реакцию отмены либо стандартный ответ.
Поведение кнопок проверяйте в каждом подключённом канале. Возможность оставить клавиатуру, убрать её или изменить подпись после нажатия зависит от канала; доступные варианты показывает редактор. Реакция на кнопку, срок действия Carry и защита записи решают разные задачи.
Проверка старого сообщения: отправьте каталог, измените название или цену, затем нажмите старую кнопку. Carry хранит данные момента отправки. Для актуальной карточки передавайте ID и перечитывайте запись после нажатия, как в уроке 1.
Подробнее: ожидание ответа, Carry и поведение кнопок. Пример перечитывания записи по ID разобран в уроке 1 выше.
Урок 6. Разделить участников на пары
Результат: одни и те же участники с одним ключом раунда дают одно и то же распределение. Сменив ключ раунда, можно получить другое распределение.
Создайте учебную коллекцию practice_participants с текстовым полем participant_id и четырьмя строками: u1, u2, u3, u4. В реакции прочитайте её действием Найти записи (record_query) в ключ participants, с лимитом 100. Это простой список строк; не подставляйте сюда целый результат нового поиска с его служебными полями.
Добавьте действие Вычисление и выберите режим Расширенная формула (formula, язык DSL). У каждой входной строки должно быть непустое уникальное поле participant_id. Используйте:
TOJSON(PAIRUP(FROMJSON({participants}); "participant_id"; "practice-round-1"))
Не заменяйте эту формулу выражением {{= ...}}: это другой движок. Буквальная проверка формулы не подставляет контекст вашей реакции, поэтому пример с {participants} проверяйте запуском учебной реакции после чтения коллекции.
Сохраните результат вычисления в pairs. Ожидается JSON-массив групп ID. Для четырёх разных участников это две пары; для пяти – одна пара и одна тройка. Проверьте, что каждый исходный ID встречается один раз. Повторите с теми же данными и ключом раунда: результат должен совпасть.
Для следующего упражнения добавьте текстовое поле tags. Укажите |team-a| у u1 и u2, |team-b| у u3 и u4. Необязательный четвёртый аргумент задаёт имя поля с тегами людей, которых нежелательно соединять, например из одного отдела:
TOJSON(PAIRUP(FROMJSON({participants}); "participant_id"; "practice-round-2"; "tags"))
Алгоритм старается избежать пересекающихся тегов, но это не гарантия отсутствия конфликтов. Если такое соседство недопустимо, дополнительно проверьте состав групп перед отправкой.
Поддерживается до 10 000 участников, но лимит отправок обычной реакции остаётся 50 на событие. Цикл по большому списку не превращается в массовую рассылку. Для уведомления участников используйте рассылки, а не тысячи отправок в одном for_each.
Подробный синтаксис и ограничения: формулы, лимиты.
Что собрать дальше
- Стоимость заказа и объединение справочников: рассчитайте сумму каждой позиции, затем добавьте данные из второго списка по общему коду.
- Повторно используемый расчёт: вынесите подготовку заказа в блок с типизированным входом и выходом, вызовите его из двух реакций и закрепите версию.
- Ответы на конфликт и ошибку: привяжите разные реакции к исходам действия и проверьте, что ошибочный путь не показывает успешный ответ.
- Страницы, итоги и совместные изменения коллекций: добавьте продолжение каталога, посчитайте по всем подходящим заказам и забронируйте последнее место без частично сохранённой заявки.
- Данные предыдущих шагов: выберите нужное поле в сообщении или текущий элемент внутри цикла.
- Проверка несохранённого черновика: задайте учебные ответы и сравните выбранные ветки без отправок и изменений рабочих данных.
Как проверить всю цепочку
Предварительный просмотр поиска и списка проверяет только одну операцию на указанных данных. Он не проверяет отправку сообщения, нажатие кнопки или изменение записи следующим действием.
Для всей несохранённой цепочки используйте проверку черновика. В отчёте сообщения и изменения показываются как предполагаемые действия на заданных учебных данных. Успешная проверка не подтверждает подключение внешнего сервиса или доставку сообщения.
После сохранения пройдите сценарий через подключённого бота и канал. Это реальный запуск сохранённых действий. В журнале выполнения проверьте источник, число найденных и показанных строк, выбранную ветку и статус изменения. Минимальный набор случаев: найдено, пусто, несколько вариантов, неполная выдача, старая кнопка, повторное подтверждение и удалённая запись.
Если правка сломала рабочую реакцию, используйте историю изменений и упражнение по восстановлению. История возвращает настройки, а не откатывает уже выполненные действия.