Создание Чат-Бот-Магазин в Телеграм с нуля. Часть вторая

Продолжение: (Часть 1 и Часть 3, Часть 4) цикла статей по созданию чат-бот-магазина в Телеграм. В этой статье мы перейдем непосредственно к практической части. Забегая вперед сообщу, что исходный код рассматриваемого блока администрирования можно скачать в конце статьи.

Созданию окружения для работы в BOT API мы уделим особое внимание, считаю это одним из основных составляющих приложения. 

Что я имею ввиду под словом "окружение": это методы запуска приложения, для работы с базой данных, метод-роутер и методы работы с BOT API. 


Настройка окружения

Рассмотрим метод запуска приложения init() - это метод, который запускает работу с базой данных и работу роутера.

<?php
    //////////////////////////////////
    // Запускаем магазин
    //////////////////////////////////
    /** Стартуем  бота
     * @return bool
     */
    public function init()
    {
        // создаем соединение с базой данных
        $this->setPdo();
        // получаем данные от АПИ и преобразуем их в ассоциативный массив
        $rawData = json_decode(file_get_contents('php://input'), true);
        // направляем данные из бота в метод роутер
        // для определения дальнейшего выбора действий
        $this->router($rawData);
        // в любом случае вернем true для бот апи
        return true;
    }
?>


В методе работы с базой данных, создадим объект PDO и запишим его в свойство $this->pdo

<?php
    /**
     *  Создаем соединение с БД
     */
    private function setPdo()
    {
        // задаем тип БД, хост, имя базы данных и чарсет
        $dsn = "mysql:host=$this->host;dbname=$this->db;charset=$this->charset";
        // дополнительные опции
        $opt = [
            // способ обработки ошибок - режим исключений
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            // тип получаемого результата по-умолчанию - ассоциативный массив
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            // отключаем эмуляцию подготовленных запросов
            PDO::ATTR_EMULATE_PREPARES => false,
            // определяем кодировку запросов
            PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
        ];
        // записываем объект PDO в свойство $this->pdo
        $this->pdo = new PDO($dsn, $this->user, $this->pass, $opt);
    }
?>


Наш роутер определяет какой тип данных нам пришел от BOT API и направляет их по заданному нами направлению. В нем мы будем использовать дополнительно еще несколько инструментов для определения статуса пользователя и в случае если это администратор бота, то проверим на цель данных. 

Для работы нашего приложения в блоке администрирования бот-магазина, мы будем обрабатывать 2 типа данных — это  message и callback_query, остальные нам пока не нужны, поэтому будем выводить предупреждение.

В тип message приходят объекты, отправленные пользователем: текст, картинки, видео, документы, аудиофайл, видеофайл и другие.  Объект callback_query приходит только при нажатии inline кнопки с опцией callback_data. Мы составим условия таким образом, что принимать message будем только текст и картинки, а callback_query будем перенаправлять в необходимый метод и в нем уже обрабатывать запрос на действие.

При проверке на текстовое сообщение мы зададим несколько шаблонов в условиях и при их выполнении направим в нужные нам методы. Это стандартная команда для старта бота /start, для страницы админа /admin и еще несколько команд для отображения экранов для просмотра категорий, контактов, добавления категории. 

Все остальные текстовые сообщения, которые пришли в бот, мы будем определять по действию, которое было записано во временную таблицу в бд и при условии, что пользователь является админом. Действия мы записываем в бд по мере вызова методов добавления категорий или товара в категории. 

При запросе на добавление категории мы запишем действие в бд как addcategory, и следующее текстовое сообщение мы будем воспринимать как название категории. В случае если будет отправлен другой контент - картинка например, то по условиям будет выведена ошибка в отправленных данных, потому что картинку мы ждем только на 5 шаге добавления товара. 

Обратите внимание как обрабатывается объект callback_query. Мы смотрим какое значение data приходит в объекте, это всегда (в нашем приложении) строка, состоящая из значений разделенные знаком нижнего подчеркивания и первым значением идет название метода в который нужно передать данные, далее это необходимые параметры в основном это идентификаторы.

<?php
    /** Роутер - Определяем что делать с запросом от АПИ
     * @param $data
     */
    private function router($data)
    {
        // берем технические данные id чата пользователя == его id и текст который пришел
        $chat_id = $this->getChatId($data);
        $text = $this->getText($data);

        // если пришли данные message
        if (array_key_exists("message", $data)) {
            // дастаем действие админа из базы
            $action = $this->getAdminAction();

            // текстовые данные
            if (array_key_exists("text", $data['message'])) {
                // если это пришел старт бота
                if ($text == "/start") {
                    $this->startBot($chat_id);
                } elseif ($text == "/admin" && $this->isAdmin($chat_id)) {
                    // выводим страницу только админу
                    $this->adminPage();
                } elseif ($text == "/admincategory" && $this->isAdmin($chat_id)) {
                    // Страница админ категорий
                    $this->adminCategory();
                } elseif ($text == "/addcategory" && $this->isAdmin($chat_id)) {
                    // отправляем на добавление категории
                    $this->addCategory();
                } elseif ($text == "/admincontact" && $this->isAdmin($chat_id)) {
                    // просмотр контактов
                    $this->adminContact();
                } else {
                    // смотрим куда отправить данные
                    if ($action == "addcategory" && $this->isAdmin($chat_id)) {
                        // если ждем данные для добавления категории
                        $this->adderCategory($text);
                    } elseif (preg_match("~^addproduct_1_~", $action) && $this->isAdmin($chat_id)) {
                        // если ждем данные для добавления товара step_1 - название
                        $param = explode("_", $action);
                        // отправляем на добавление описания
                        $this->addProductName($param['2'], $text);
                    } elseif (preg_match("~^addproduct_2_~", $action) && $this->isAdmin($chat_id)) {
                        // если ждем данные для добавления товара step_2 - описание
                        $param = explode("_", $action);
                        // отправляем на добавление описания
                        $this->addProductDescription($param['2'], $param['3'], $text);
                    } elseif (preg_match("~^addproduct_3_~", $action) && $this->isAdmin($chat_id)) {
                        // если ждем данные для добавления товара step_3 - единица измерения
                        $param = explode("_", $action);
                        // отправляем на добавление описания
                        $this->addProductPrice($param['2'], $param['3'], $text);
                    } elseif (preg_match("~^addproduct_4_~", $action) && $this->isAdmin($chat_id)) {
                        // если ждем данные для добавления товара step_4 - цена
                        $param = explode("_", $action);
                        // отправляем на добавление описания
                        $this->addProductUnit($param['2'], $param['3'], $text);
                    } elseif (preg_match("~^addcontact_~", $action) && $this->isAdmin($chat_id)) {
                        // если ждем данные для для редактирования контактов
                        $param = explode("_", $action);
                        // отправляем данные на редактирование контактов
                        $this->rederContact($param[1], $text);
                    } else {
                        $this->sendMessage($chat_id, "Нам пока не нужны эти данные. Спасибо.");
                    }
                }
            } elseif (array_key_exists("photo", $data['message'])) {
                // если пришли картинки
                if (preg_match("~^addproduct_5_~", $action) && $this->isAdmin($chat_id)) {
                    // если ждем данные для добавления товара step_5 - картинка
                    $param = explode("_", $action);
                    // берем данные картинки
                    $file_id = end($data['message']['photo'])['file_id'];
                    // отправляем на добавление описания
                    $this->addProductPhoto($param['2'], $param['3'], $file_id);
                } else {
                    $this->sendMessage($chat_id, "Нам пока не нужны эти данные. Спасибо.");
                }
            } else {
                $this->sendMessage($chat_id, "Нам пока не нужны эти данные. Спасибо.");
            }
        } // если пришел запрос на функцию обратного вызова
        elseif (array_key_exists("callback_query", $data)) {
            // смотрим какая функция вызывается
            $func_param = explode("_", $text);
            // определяем функцию в переменную
            $func = $func_param[0];
            // вызываем функцию передаем ей весь объект
            $this->$func($data['callback_query']);
        } // Здесь пришли пока не нужные нам форматы
        else {
            // вернем текст с ошибкой
            $this->sendMessage($chat_id, "Нам пока не нужны эти данные. Спасибо.");
        }
    }
?>


Для работы с действиями администратора есть несколько методов для выборки, для записи и для очистки.

<?php
    /** Отменяем все действия админа
     * @return mixed
     */
    private function adminActionCancel()
    {
        // возвращаем результат запроса
        return $this->pdo->query("DELETE FROM bot_shop_action_admin");
    }

    /** Получаем действие админа из таблицы
     * @return bool
     */
    private function getAdminAction()
    {
        // достаем из базы
        $last = $this->pdo->query("SELECT name FROM bot_shop_action_admin ORDER BY id DESC LIMIT 1");
        // преобразуем строку в массив
        $lastAction = $last->fetch();
        // если есть значение то возвращаем его иначе false
        return isset($lastAction['name']) ? $lastAction['name'] : false;
    }

    /** Записываем действие админа
     * @param $action
     * @return mixed
     */
    private function setActionAdmin($action)
    {
        // отменяем все действия админа
        if ($this->adminActionCancel()) {
            // готовим запрос
            $insertSql = $this->pdo->prepare("INSERT INTO bot_shop_action_admin SET name = :name");
            // возвращаем результат
            return $insertSql->execute(['name' => $action]);
        } else {
            // выводим ошибку
            $this->sendMessage($this->admin, "Ошибка отмены предыдущих действий.");
        }
    }
?>


В случае с работой по наполнению категории товаром, действие пишем с дополнительными параметрами напрмиер addproduct_3_2_1, где:

  • addproduct - название действия
  • 3 - шаг действия - №3 у нас это определено как добавление еденицы измерения
  • 2 - id категории
  • 1 - id товара в базе

Таким образом будет удобно работать, мы понимаем, какое действие идет, на каком уже шаге, в какой категории мы работаем и с каким объектом товара.


Структура таблиц в бд и этапы добавления товара

Как и предполагалось в структуре базы данных прошли изменения. Были добавлены 2 таблицы для записи ожидаемых действий администратора при работе по наполнению товаров в бот-магазине и таблица временных данных по товару при добавлении его в категорию. Добавление товара происходит в 5 этапов (шагов):

  1. Добавление названия 
  2. Добавление описания
  3. Добавление стоимости
  4. Добавление единицы измерения
  5. Добавление картинки

После успешной обработки текстового сообщения на шаге 1, идет запись во временную таблицу товара и в таблицу действий, о том, что уже шаг 2 и так далее до шага 5 при загрузке картинки. Когда приходит картинка то мы ее загружаем на свой сервер и в случае успеха делаем запись в основную таблицу товаров переписываем в нее все что было добавлено на всех шагах добавления товара далее удаляем из временной таблицы товаров запись и отменяем все действия админа в таблице действий, по итогу выводим на просмотр категорию.

Еще изменил кодировку таблиц для работы с емодзи. Теперь она utf8mb4_unicode_ci.

--
-- Структура таблицы `bot_shop_action_admin`
--

CREATE TABLE IF NOT EXISTS `bot_shop_action_admin` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------

--
-- Структура таблицы `bot_shop_basket`
--

CREATE TABLE IF NOT EXISTS `bot_shop_basket` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `product_id` int(11) DEFAULT NULL,
  `product_count` int(11) DEFAULT NULL,
  `user_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------

--
-- Структура таблицы `bot_shop_category`
--

CREATE TABLE IF NOT EXISTS `bot_shop_category` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `hide` int(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------

--
-- Структура таблицы `bot_shop_contact`
--

CREATE TABLE IF NOT EXISTS `bot_shop_contact` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `description` text COLLATE utf8mb4_unicode_ci,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

--
-- Дамп данных таблицы `bot_shop_contact`
--

INSERT INTO `bot_shop_contact` (`id`, `description`) VALUES
(1, 'Контакт');

-- --------------------------------------------------------

--
-- Структура таблицы `bot_shop_product`
--

CREATE TABLE IF NOT EXISTS `bot_shop_product` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `parent` int(11) DEFAULT NULL,
  `name` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `description` text COLLATE utf8mb4_unicode_ci,
  `image_tlg` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `image` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `price` decimal(10,2) DEFAULT NULL,
  `unit` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `hide` int(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------

--
-- Структура таблицы `bot_shop_product_temp`
--

CREATE TABLE IF NOT EXISTS `bot_shop_product_temp` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `parent` int(11) DEFAULT NULL,
  `name` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `description` text COLLATE utf8mb4_unicode_ci,
  `image_tlg` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `image` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `price` decimal(10,2) DEFAULT NULL,
  `unit` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------

--
-- Структура таблицы `bot_shop_profile`
--

CREATE TABLE IF NOT EXISTS `bot_shop_profile` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) DEFAULT NULL,
  `first_name` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `last_name` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `phone` varchar(15) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `adress` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `action` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Интерфейс блока администрирования

Несколько скринов интерфейса.

Телеграм бот-магазин

Телеграм бот-магазин

Телеграм бот-магазин

Телеграм бот-магазин


Подводим итоги

Весь код в статье расписывать не буду, просто получилось 1300 строк из которых 500 это комментарии, старался максимально подробно комментрировать, чтобы было понятно. Реализованные возможности приложения по результату второй части:

  1. Определение ролей: администратор и пользователь
  2. Добавление, определение видимости в каталоге и удаление категорий.
  3. Добавление, определение видимости в категории и удаление товара в категории.
  4. Редактирование данных контактов бот-магазина.

Исходный код текущей версии бот-магазина можно скачать. В следующей статье мы будем рассматривать интерфейс пользовательской части магазина: редактирование профиля, просмотр каталога, добавление товара в корзину, управление корзиной. 


Комментарии

Авторизуйтесь через Telegram, чтобы оставить комментарий.
Откройте бот @SiteAuthBot, нажмите кнопку Старт/Start. Следуйте инструкциям бота.


  • как выключить админку что бы только пользовательский интерфейс был.
    Greg 29.04.2019 в 16:34
    • Просто не указывайте в настройках id админа -> поставьте 0, и не будет админки
      iMakeBots 29.04.2019 в 16:38