FSM-диалоги в aiogram 3: бот для записи клиентов — KEL IT
Telegram боты 4 мин чтения

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.

KEL IT

Нужна разработка под ключ?

Я занимаюсь такими проектами профессионально. Telegram-боты, Mini Apps, сайты, мобильные и десктопные приложения. Расскажите о задаче — разберём и предложим решение.