FSM-диалоги в aiogram 3: бот для записи клиентов
Большинство полезных Telegram-ботов — не одна команда /start, а цепочка шагов. Пользователь выбирает услугу → дату → время → подтверждает запись. Именно для этого в aiogram 3 есть FSM (Finite State Machine) — конечный автомат состояний.
В этой статье разберём, как построить бот записи клиентов с нуля: с состояниями, inline-кнопками, хранилищем и уведомлениями администратору.
Что такое FSM и зачем он нужен
FSM — это способ отслеживать, на каком шаге диалога находится пользователь. Без FSM бот не знает, ответил ли пользователь уже на первый вопрос или только начал диалог.
В aiogram 3 FSM работает через:
State— объявление шаговStatesGroup— группировка состоянийStateFilter— маршрутизация хэндлеров по текущему состояниюFSMContext— хранение данных диалога (название, дата, etc.)
Если вам нужна подобная разработка под ключ — напишите в Telegram, разберём задачу и предложим решение.
Структура проекта
booking_bot/
├── bot.py # запуск и диспетчер
├── handlers/
│ ├── booking.py # FSM-хэндлеры записи
│ └── admin.py # уведомления и список записей
├── states.py # StatesGroup
├── keyboards.py # inline и reply клавиатуры
└── storage.py # Redis или MemoryStorage
Определяем состояния
# states.py
from aiogram.fsm.state import State, StatesGroup
class BookingForm(StatesGroup):
choose_service = State()
choose_date = State()
choose_time = State()
enter_name = State()
enter_phone = State()
confirm = State()
Каждое State() — это шаг диалога. aiogram автоматически управляет переходами между ними.
Хэндлеры диалога
# handlers/booking.py
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext
from aiogram.filters import StateFilter
from states import BookingForm
from keyboards import services_kb, dates_kb, confirm_kb
router = Router()
# Шаг 1 — выбор услуги
@router.message(F.text == "📅 Записаться")
async def start_booking(message: Message, state: FSMContext):
await state.set_state(BookingForm.choose_service)
await message.answer(
"Выберите услугу:",
reply_markup=services_kb()
)
# Шаг 2 — выбор даты
@router.callback_query(StateFilter(BookingForm.choose_service))
async def process_service(callback: CallbackQuery, state: FSMContext):
await state.update_data(service=callback.data)
await state.set_state(BookingForm.choose_date)
await callback.message.edit_text(
"Выберите удобный день:",
reply_markup=dates_kb()
)
# Шаг 4 — имя
@router.callback_query(StateFilter(BookingForm.choose_time))
async def process_time(callback: CallbackQuery, state: FSMContext):
await state.update_data(time=callback.data)
await state.set_state(BookingForm.enter_name)
await callback.message.edit_text("Введите ваше имя:")
# Шаг 5 — телефон
@router.message(StateFilter(BookingForm.enter_name))
async def process_name(message: Message, state: FSMContext):
await state.update_data(name=message.text)
await state.set_state(BookingForm.enter_phone)
await message.answer("Введите номер телефона (или нажмите поделиться):")
# Шаг 6 — подтверждение
@router.message(StateFilter(BookingForm.enter_phone))
async def process_phone(message: Message, state: FSMContext):
await state.update_data(phone=message.text)
data = await state.get_data()
await state.set_state(BookingForm.confirm)
text = (
f"✅ Проверьте данные:\n\n"
f"Услуга: {data['service']}\n"
f"Дата: {data['date']}\n"
f"Время: {data['time']}\n"
f"Имя: {data['name']}\n"
f"Телефон: {data['phone']}"
)
await message.answer(text, reply_markup=confirm_kb())
Хранилище состояний
Для продакшна используйте Redis — он не теряет состояния при перезапуске бота:
# bot.py
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.redis import RedisStorage
import asyncio
bot = Bot(token="YOUR_TOKEN")
storage = RedisStorage.from_url("redis://localhost:6379")
dp = Dispatcher(storage=storage)
async def main():
dp.include_router(booking_router)
await dp.start_polling(bot)
asyncio.run(main())
Для разработки достаточно MemoryStorage() — данные хранятся в памяти процесса.
Уведомления администратору
После подтверждения записи бот должен уведомить администратора:
@router.callback_query(F.data == "confirm", StateFilter(BookingForm.confirm))
async def finish_booking(callback: CallbackQuery, state: FSMContext, bot: Bot):
data = await state.get_data()
await state.clear() # Сбрасываем FSM
# Уведомление клиенту
await callback.message.edit_text(
"🎉 Запись подтверждена!\nМы свяжемся с вами для подтверждения."
)
# Уведомление администратору
admin_text = (
f"📋 Новая запись!\n\n"
f"Услуга: {data['service']}\n"
f"Дата: {data['date']} в {data['time']}\n"
f"Клиент: {data['name']}\n"
f"Телефон: {data['phone']}\n"
f"User ID: {callback.from_user.id}"
)
await bot.send_message(ADMIN_CHAT_ID, admin_text)
Обработка отмены
Важно дать пользователю возможность выйти из диалога в любой момент:
@router.message(F.text == "❌ Отмена")
async def cancel_booking(message: Message, state: FSMContext):
current = await state.get_state()
if current is not None:
await state.clear()
await message.answer("Запись отменена.", reply_markup=main_menu_kb())
Нужна помощь с реализацией? Я занимаюсь этим профессионально. Написать в Telegram → или vic.kell@ya.ru
FAQ
Можно ли использовать FSM без Redis?
Да, MemoryStorage() достаточно для тестирования. Но в продакшне данные исчезнут при перезапуске — обязательно переключитесь на Redis.
Сколько состояний можно создать в одном StatesGroup?
Практически неограниченно. На практике диалоги из 3–10 шагов — оптимальны для UX.
Как передать данные из FSM в базу данных?
После state.get_data() вы получаете словарь — передайте его в вашу ORM (SQLAlchemy, tortoise-orm) или напрямую в PostgreSQL через asyncpg.
Можно ли запускать несколько параллельных диалогов?
Да, FSM в aiogram привязан к user_id + chat_id, поэтому каждый пользователь имеет собственное состояние независимо от других.
Как тестировать FSM-хэндлеры?
Используйте pytest-asyncio и мок-объекты для Message, CallbackQuery и FSMContext. aiogram предоставляет тестовые утилиты в aiogram.utils.test.