Главная → Боты → кейс «Бот приёма записей»

Telegram-бот приёма записей: как он устроен и чем проверяется

демонстрационная работа Бот сделал я, для портфолио и для себя. Реальных клиентов через него не водил — зато вся логика расписания покрыта самопроверкой, которую может запустить любой, у кого есть Python.

1 файл~300 строк, без фреймворков
0 зависимостейтолько стандартная библиотека Python
13 проверокпроходят без токена и без интернета
от 990 ₽шаблон архивом, до 6 000 ₽ под ключ
Что в разборе
  1. Для кого такая запись
  2. Схема хранения
  3. Как считается свободное время
  4. Главная функция: проверка пересечения
  5. Отмена и защита чужой записи
  6. Напоминания ровно один раз
  7. 13 самопроверок
  8. Установка и запуск
  9. Цены и что в них входит

1. Для кого такая запись

Сценарий повторяется у любой точки, где визит идёт по времени: салон, барбер, косметолог, репетитор, стоматолог, автомасляный сервис. Сейчас это выглядит так: клиент пишет в мессенджер, владелец отвечает вручную, ведёт список в заметках и хотя бы раз в неделю сажает двух клиентов на одно время.

Бот закрывает три вещи: клиент сам выбирает услугу и видит только свободное время; два клиента не могут занять пересекающееся окно — это проверяется кодом, а не вниманием человека; владелец одной командой получает журнал записей в CSV, который открывается в Excel без настройки кодировки.

2. Схема хранения: одна база, две таблицы

SQLite — это файл на диске, сервер не нужен. Таблицы создаются при первом запуске:

CREATE TABLE IF NOT EXISTS services(
    id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, price INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS bookings(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    chat_id INTEGER NOT NULL,
    service_id INTEGER NOT NULL REFERENCES services(id),
    starts TEXT NOT NULL,                    -- ISO datetime
    minutes INTEGER NOT NULL,
    status TEXT NOT NULL DEFAULT 'active',   -- active | canceled | done
    reminded INTEGER NOT NULL DEFAULT 0,
    created TEXT NOT NULL);
CREATE INDEX IF NOT EXISTS ix_book_starts ON bookings(starts, status);

Три решения здесь важные. row_factory = sqlite3.Row — строки доступны по именам колонок, и код не сползает в row[3]. Отдельная колонка minutes, а не жёстко «час», — услуга на 90 минут ложится в ту же схему. Флаг reminded — чтобы напоминание отправилось ровно один раз и не зависело от того, перезапускался процесс или нет.

3. Расписание считается на лету

В базе хранятся только занятые начала. Сетка дня строится каждый раз заново, поэтому «перенесите рабочий день на 9 утра и уберите обед» — это правка двух констант, а не переписывание таблиц.

WORK_START, WORK_END, SLOT_MINUTES = 10, 19, 60

def free_slots(con, day_iso):
    taken = busy(con, day_iso)
    return ["%02d:00" % h for h in range(WORK_START, WORK_END)
            if "%sT%02d:00:00" % (day_iso, h) not in taken]

4. Главная функция: проверка пересечения интервалов

Здесь вся суть задачи, и именно за это платят деньги. Наивная реализация («занят ли слот 14:00?») ломается на услуге в полтора часа: клиент записался в 13:00 на 90 минут, а система считает 14:00 свободным.

def book(con, chat_id, service_id, when_iso, minutes=SLOT_MINUTES):
    start = dt.datetime.fromisoformat(when_iso)
    end   = start + dt.timedelta(minutes=minutes)
    for r in con.execute("SELECT starts, minutes FROM bookings WHERE status='active' AND starts LIKE ?",
                         (start.date().isoformat() + "%",)):
        s = dt.datetime.fromisoformat(r["starts"])
        e = s + dt.timedelta(minutes=r["minutes"])
        if start < e and s < end:      # классическая проверка пересечения
            return None                # слот занят — честный отказ
    # ...INSERT и возврат id записи

Условие start < e and s < end даёт три разных ответа: 14:00 поверх 13:30–14:30 — отказ; 15:00 сразу после 14:00–15:00 — запись проходит, потому что стык-в-стык пересечением не считается; 13:00 перед 14:00 — тоже проходит. Если написать <=, второй случай сломается: клиенты не смогут записываться плотным потоком, и владелец потеряет один-два визита в день. Один символ — реальные деньги, поэтому он покрыт проверкой №6 из самопроверки.

5. Отмена, которая не даёт отменить чужую запись

UPDATE bookings SET status='canceled'
 WHERE id=? AND chat_id=? AND status='active'

Номер записи можно угадать, а вот отменить чужой визит нельзя: условие по chat_id — это guard. Запись не удаляется, а переходит в статус canceled: история сохраняется, освободившееся время снова попадает в свободные слоты.

6. Напоминания ровно один раз

Отдельный режим --remind выбирает активные записи на ближайшие N часов, где reminded=0, отправляет сообщение и ставит флаг. Он запускается по расписанию (раз в час), а не крутится внутри основного цикла: упавший процесс опроса не должен тянуть за собой напоминания, и никто не получает спам повтором.

7. Тринадцать самопроверок одной командой

$ python bot_booking.py --check
OK: логика бота проверена — 13 проверок (слоты, пересечения, отмена, напоминания, CSV)

Что именно проверяется: база поднимается и услуги засеиваются; сетка дня содержит 9 слотов; первая запись создаётся; занятый слот исчезает из свободных; пересекающееся время возвращает None; стык-в-стык разрешается; отмена работает; повторная отмена возвращает False; после отмены слот вернулся; чужую запись отменить нельзя; напоминание срабатывает один раз; CSV выгружается непустым и с кириллицей; /start отвечает текстом, а не падает.

Тесты идут на отдельной временной базе и боевую не трогают. Это тот раздел, который стоит показывать заказчику скриншотом: «вот работает, и вот чем проверяется» вместо обещаний «должно работать».

8. Установка: что нужно, чтобы он ожил

  1. Написать в Telegram @BotFather, команда /newbot, имя и username (заканчивается на bot) — токен придёт сообщением.
  2. Положить токен в переменную окружения BOT_TOKEN и запустить python bot_booking.py.
  3. Поставить автозапуск: на Windows — задача планировщика, на Linux — systemd-юнит с Restart=always. В коде токен не хранится и в скриншотах не светится.
  4. Отдельной задачей раз в час запускать --remind.

Никакого публичного адреса и веб-сервера не нужно: бот сам ходит за обновлениями (long-polling), поэтому работает даже за домашним NAT.

9. Цены и что в них входит

ВариантЦенаЧто получает заказчик
Шаблон архивом990 ₽Исходник, инструкция по установке, самопроверка. Настройка на себя — ваша.
С установкой2 900 ₽То же плюс я ставлю бота на ваш компьютер или сервер, регистрирую токен, проверяю на ваших услугах.
Под ключот 6 000 ₽Ваши услуги и цены, ваше расписание и выходные, ваши тексты напоминаний, журнал в нужном формате, 7 дней правок.
Доработкипо задачеОплата в боте, буфер между визитами, второй админ-аккаунт, веб-интерфейс на тех же таблицах.

Честно об ограничениях: один процесс — одна точка; на сеть салонов нужна колонка на локацию и другой порядок развёртывания. Оплата в базовой версии не принимается — это отдельный этап с кассой или платёжным сервисом, и стоит он другого чека.

Хотите такого бота под свою точку?

Напишите список услуг, часы работы и длину визита. Сначала покажу бота в деле на демо-базе, дальше решаем. Созвонов не веду — всё в переписке.

Написать на Kwork Другие сценарии ботов

Работа написана и проверена 30.10.2026, команда --check проходит на Python без установки сторонних пакетов. Демо-записей реальных клиентов в комплекте нет, и я не выдаю тестовые слоты за клиентские.