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

Продолжение статей : (Часть 1Часть 2, Часть 3)

После оформления заказа добавим возможность оплатить заказ через Яндекс.Деньги на пользовательский кошелек. Для этого заменим кнопку, которая была в предыдущих статьях на экране просмотра заказа. Установим кнопку с ссылкой на страницу Яндекс.Денег с предзаполненой формой оплаты. Форма заполняется переданными в ссылке данными, саму ссылку генерирует метод getUrl

<?php
    /** Формируем ссылку для оплаты
     * @param $sum
     * @param $user_id
     * @param $order_id
     * @return string
     */
    private function getUrl($sum, $user_id, $order_id)
    {
        return "https://money.yandex.ru/quickpay/confirm.xml?receiver=" . $this->receiver
            . "&quickpay-form=shop&targets=" . urlencode($this->nameShop)
            . "&paymentType=AC&sum=" . $sum
            . "&label=" . $user_id . ":" . $order_id . ":" . md5(rand(0, 1000))
            . "&comment=" . urlencode("Оплата заказа #" . $order_id)
            . "&successURL=" . $this->urlBot;
    }

 

Кнопку заменим в методе drawOrder

<?php
    /** Рисуем заказ
     * @param $user_id
     * @param $begin
     * @return array
     */
    private function drawOrder($user_id, $begin)
    {

        // ...... 


            if(!$orderRaw['status']) {
                // готовим кнопку для перехода в Яндекс.Деньги
                $url = $this->getUrl($total, $user_id, $orderRaw['id']);
                $buttons[][] = $this->buildInlineKeyBoardButton('Оплатить через Яндекс.Деньги', '', $url);
            } else {
                // если заказ оплачен то уведомляем
                $text .= "\n<b>Заказ оплачен</b>\n";
            }


       // .......

    }

 

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

<?php
    // Яндекс.Кошелек для приема оплаты
    private $receiver = "ВАШ_ЯНДЕКС_КОШЕЛЕК";
    // адрес на который переадресует пользователя в случае успешного платежа
    private $urlBot = "t.me/forarticlebot";
    // название магазина
    private $nameShop = "Телеграм Чат-Бот-Магазин";

 


HTTP уведомления

Теперь нам необходимо в настройках Яндекс.Денег настроить прием уведомлений на специальный адрес обработчика, который будет уведомлять пользователя и администратора о совершенном платеже, и в случае успешного выполнения задуманного сценария - менять статус заказа. 

Для этого на странице https://money.yandex.ru/myservices/online.xml необходимо указать путь до файла ya_success.php - перед сохранением изменений убедитесь, что чекбокс установлен у "Отправлять уведомления". Вы можете дополнительно сделать проверку уведомлений использовав секретное слово.

Файл - ya_success.php

<?php
// отсекаем лишние запросы
if ($_SERVER["REQUEST_METHOD"] != "POST") {
    echo "Hello world!";
    exit();
}

// запускаем
new Yandex();

/**
 * Class Yandex
 */
class Yandex
{
    private $data;

    // первичные данные
    private $token = "ВАШ_ТОКЕН";
    private $admin = 0;

    // для соединения с БД
    private $host = 'localhost';
    private $db = '';
    private $user = '';
    private $pass = '';
    private $charset = 'utf8mb4';

    /**
     * @var PDO
     */
    private $pdo;

    public function __construct()
    {
        $this->data = $_POST;
        $this->setPdo();
        $this->getPay();
    }

    /** Получаем Label
     * @return mixed
     */
    private function getLabel()
    {
        return $this->data['label'];
    }

    /** Получаем id пользователя
     * @return mixed
     */
    private function getIdUser()
    {
        return explode(":", $this->getLabel())[0];
    }

    /** Получаем id заказа
     * @return mixed
     */
    private function getIdOrder()
    {
        return explode(":", $this->getLabel())[1];
    }

    /** Получаем сумму
     * @return mixed
     */
    private function getFullSum()
    {
        return $this->data['withdraw_amount'];
    }

    /**
     *  Принимаем оплату
     */
    private function getPay()
    {
        // делаем запрос в бд по пришедшим данным
        $order = $this->pdo->prepare("SELECT * FROM bot_shop_order WHERE user_id = :user_id AND id = :order_id AND status = 0");
        $order->execute(['user_id' => $this->getIdUser(), 'order_id' => $this->getIdOrder()]);
        // если запись в бд найдена
        if ($order->rowCount() > 0) {
            // запрос на обновление статуса заказа
            $update = $this->pdo->prepare("UPDATE bot_shop_order SET status = 1 WHERE id = :id");
            // если обновили то уведомляем
            if ($update->execute(['id' => $this->getIdOrder()])) {
                // шлем пользователю уведомление о заказ оплачен
                $this->sendMsg([
                    'chat_id' => $this->getIdUser(), 
                    'text' => "Заказ #" . 
                        $this->getIdOrder() . " успешно оплачен. В ближайшее время с вами свяжется менеджер."]);
                // шлем алмину уведомление о заказ оплачен
                $this->sendMsg([
                    'chat_id' => $this->getIdUser(), 
                    'text' => "Заказ #" . 
                        $this->getIdOrder() . " успешно оплачен. Сумма " . 
                        $this->getFullSum() . "."]);
            } else {
                // шлем пользователю уведомление о том что пришли деньги за заказ, но есть проблемы
                $this->sendMsg([
                    'chat_id' => $this->getIdUser(), 
                    'text' => 'Пришли деньги ' . 
                        $this->getFullSum() . ' РУБ, за заказ ' . 
                        $this->getIdOrder() . ' но не удалось обновить статус заказа. С вами свяжется менеджер.']);
                // шлем админу уведомление о том что пришли деньги за заказ, но есть проблемы
                $this->sendMsg([
                    'chat_id' => $this->admin, 
                    'text' => 'Пришли деньги ' . 
                        $this->getFullSum() . ' РУБ, за заказ ' . 
                        $this->getIdOrder() . ' от пользователя ' . 
                        $this->getIdUser() . ' но не удалось обновить статус заказа.']);
            }
        } else {
            // шлем админу уведомление о том что пришли какие-то деньги
            $this->sendMsg([
                    'chat_id' => $this->admin, 
                    'text' => 'Пришли деньги ' . 
                        $this->getFullSum() . ' руб, но назначение не понятно ']);
        }
    }

    /** Отправляем сообщение
     * @param $fields
     * @return mixed
     */
    public function sendMsg($fields)
    {
        $ch = curl_init('https://api.telegram.org/bot' . $this->token . '/sendMessage');
        curl_setopt_array($ch, array(
            CURLOPT_POST => 1,
            CURLOPT_POSTFIELDS => http_build_query($fields),
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_TIMEOUT => 10
        ));
        $r = json_decode(curl_exec($ch), true);
        curl_close($ch);
        return $r;
    }

    /**
     *  Соединение с базой
     */
    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"
        ];
        $this->pdo = new PDO($dsn, $this->user, $this->pass, $opt);
    }
}

 


Исходные файлы бота

Это была последняя часть по строительству Чат-Бот-Магазина в Телеграм. Стиль программирования со временем конечно у меня меняется (прошло, наверное, больше полгода с начала первой статьи) и уже если честно, то прикручивать оплату в этот проект бота было уже не привычно, даже я скажу: "Как вообще я мог так пис`ать?", но чтобы не ломать логику настроенную в самом начале - пришлось подстраиваться. 

По итогу "скелет" бот-магазина готов. По вашим потребностям вы можете прикручивать любой функционал, менять и использовать этого бота без каких-либо ограничений. Огромная просьба не использовать бот в целях, которые нарушают законы. 


Комментарии

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


  • Спасибо! Отличная статья!
    Юрий(НСП) 10.04.2019 в 20:07
    • Спасибо.
      iMakeBots 10.04.2019 в 20:08
  • Отлично! это лучшее что есть в сети, видно что человек любит своё дело)) Это шедевр! 10 из 10!
    Willgellm Altarius 11.04.2019 в 13:38
    • Спасибо. Дальше еще интереснее будет ))
      iMakeBots 11.04.2019 в 13:39
  • Когда часть 5?)))
    insta 4at 16.05.2019 в 02:17
    • Всего по плану было 4 части
      iMakeBots 16.05.2019 в 13:54
  • Спасибо за статью! Не подскажете, как можно изменить инлайн в обычную клавиатуру?
    Khurshid Akhmedjanov 17.05.2019 в 08:02
    • Ничего сложно в выводе клавиатуры вместо кнопок нет, просто вам этого будет не достаточно, ведь кнопка клавиатуры отправляет объект message, а инлайн кнопка объект callback_query - и необходимо сделать метод, который будет ловить команду и обрабатывать по вашему сценарию.

      Все кнопки отправляются в АПИ одинаково в параметре reply_markup - только со своими параметрами:

      Массив данных (
          'Тип клавиатуры' => 
              Массив строк кнопок (
                  Массив кнопок строки (
                      Массив параметров кнопки (
                  
                      )
                  )
               ),
          // если предусмотрено 
          'Дополнительный параметр',
      )
      iMakeBots 17.05.2019 в 10:32
  • Не подскажите как добавить способ оплаты "Наличными при получении"? чтоб было 2 способа оплаты, Яндекс. Деньги и наличные
    A M 21.06.2019 в 23:43
    • Просто вывести кнопку "Наличными при получении" просто. Но нужен еще обработчик этой кнопки.

      private function drawOrder($user_id, $begin)
          {
              // ......
              $buttons[] = [
                   $this->buildInlineKeyBoardButton('Оплатить через Яндекс.Деньги', '', $url),
                   $this->buildInlineKeyBoardButton('Наличными при получении', 'getNal_' . $orderRaw['id'])
              ];
          }
      
      /**
      * Обработчик кнопки "За наличные"
      */
      public function getNal($data) 
          {
              // ......
              // Получаем id заказа через данные от кнопки
              $param = explode("_", $data['data']);
              $order_id = $param[1];
              // Далее логика - например записать в бд 
              // что этот заказ будет оплачен наличными, 
              // выслать инструкцию ...
          }
      
      iMakeBots 22.06.2019 в 09:08
      • Благодарю, а не подскажите как реализовать следующую вещь. Допустим у клиента несколько адресов доставки или несколько получателей заказа, допустим 3 (корзина одна, а получателй несколько), как реализовать клонирование полей ввода для клиента? Чтоб нажимая кнопку "Добавить нового получателя" дублировались Телефон1/Адрес1/Имя1, затем нажимая "Добавить нового получателя" процесс повторялся до того момента пока пользователь не внесет всех получателей?

        Я так понимаю это тут надо делать?
          
        private function savePhoneUser($text, $data)
            {
                $user_id = $this->getChatId($data);
                // проверяем телефон
                if (preg_match("/^ [0-9]{9,14}$/i", $text)) {
                    if ($this->setActionUser("step_2_adress", $user_id)) {
                        if ($this->setParamUser('phone', $text, $user_id)) {
                            $text_ = "<b>Оформление заказа</b>nn";
                            // сумма заказа
                            $text_ .= "Сумма заказа: " . $this->totalSumOrder($user_id) . " рублей";
                            // телефон
                            $text_ .= "nТелефон: " . $text;
                            $text_ .= "nnУкажите ваш адрес для доставки:";
        

        И еще один вопрос (если не сложно) как сделать информирование пользователя о том, что его заказ принят и/или исполнен? К примеру чтоб админ нажимал на кнопку "заказ принят", "заказ выполнен" или любую другую, и пользователь получал в свой телеграм или на почту уведомление о том, что его заказ "такой-то" принят или выполнен?
        A M 22.06.2019 в 22:51
        • Вопрос 1:
          Если вы запланируете в сценарии оформления заказа возможность выбора адреса доставки из множества, то в отсутствии хотя бы одного заполненного профиля места доставки по умолчанию просто запрос данных, если уже есть минимум один то возможность выбрать из имеющихся + возможность заполнить новый.
          Да, это можно сделать на этом участке

          private function savePhoneUser($text, $data)
          Вопрос 2:
          Для таких уведомлений надо реализовать администрирование заказов, предусмотреть смену статусов и при смене статуса в зависимости от сценария производить уведомления или какие-либо другие действия.
          iMakeBots 23.06.2019 в 12:59
      • Да и не могли бы вы вот этот момент расписать если не сложно

        " // Далее логика - например записать в бд
        // что этот заказ будет оплачен наличными,
        // выслать инструкцию ..."
        Я как бы чайник, и в этом не особо шарю, да и другим людям это тоже пригодиться. Спасибо
        A M 22.06.2019 в 23:23
        • Расписать это можно по разному, у каждого своя модель взаимодействия )

          Этот участок кода включается когда пользователь нажал на кнопку для оплаты наличными, по моей логике должно происходить следующее:
          1. Установить флаг в заказе о том что оплата будет за наличные
          2. Вывести на экран инструкцию как это будет происходить: " ... с вами свяжется менеджер для подтверждения заказа, после курьер вам доставит товар и вы произведете оплату на месте вручения товара ..."
          3. К инструкции я еще бы добавил кнопку на переход в личный кабинет для просмотра заказа в нем
          iMakeBots 23.06.2019 в 12:44
          • Спасибо, за ответы, но я повторяю - я не программист понимаете? Мне бы ваши ответы выше в виде кода с пояснением что, куда подставить и не только мне эти ответы были бы полезны, не думаю что у вас аудитория данного блога состоит из профи, я думаю что все же основная масса сюда зашедших это "чайники" как я к примеру, было бы здорово если бы вы максимально в сторону чайников ориентировались, потому что чайники не знают что, куда и как писать, они просто хотят готового решения.

            К примеру вот тут:
            private function savePhoneUser($text, $data)
            Я у вас решения спрашивал, я не знаю как это реализовать самостоятельно.

            И касательно вот этого:
            
            Этот участок кода включается когда пользователь нажал на кнопку для оплаты наличными, 
            по моей логике должно происходить следующее:
            1. Установить флаг в заказе о том что оплата будет за наличные
            2. Вывести на экран инструкцию как это будет происходить: 
            " ... с вами свяжется менеджер для подтверждения заказа, после курьер вам доставит товар 
            и вы произведете оплату на месте вручения товара ..."
            3. К инструкции я еще бы добавил кнопку на переход в личный кабинет для просмотра заказа в нем
            

            Я как бы тоже могу диссертацию написать, но хотелось бы решения потому что я лично не программист повторяю и я вообще не понимаю как вот это сделать
            "1. Установить флаг в заказе о том что оплата будет за наличные
            2. Вывести на экран инструкцию как это будет происходить: " ... с вами свяжется менеджер для подтверждения заказа, после курьер вам доставит товар и вы произведете оплату на месте вручения товара ..."
            3. К инструкции я еще бы добавил кнопку на переход в личный кабинет для просмотра заказа в нем".

            Представьте что вы сдали телефон в ремонт или пришли подстригаться, а вас на половину подстригли и сказали, ну домой потом придешь сам себя подравняешь, лишнее уберешь и я бы еще вот это вот добавил, ну ладно мне пора, пока...
            З.Ы: добавьте на сайт какой нибудь способ сделать пожертвования в пользу вас/вашего проекта.
            A M 23.06.2019 в 16:52
            • Прекрасно понимал, когда писал ответы, что вы хотите рабочий код вместо текста. В комментариях все что вы хотите не запилить - все запутаются куда и чего копировать\вставлять, это отдельная статья.

              Также понимаю, что готовые решения с комментированием процесса для новичков самое желаемое в сети. Все статьи на сайте ориентированы именно на них. Люди, которые понимают, что они хотят и как это сделать - сюда не заглядывают, и если бы для них стал писать то применял кучу библиотек и использовал фреймворки.

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

              Устанавливать донат пока в планах нет.
              iMakeBots 23.06.2019 в 17:44
      • Доброго времени суток, я сделал как вы написали, но ничего не происходит, см. скриншот, просто появляется таймер после клика на кнопку и все, код обработчика тот же что и у вас. в чем может быть проблема? обработчик в любое место вставляется? я его вставил после
        private function getType($data)
            {
                if (isset($data['callback_query'])) {
                    return "callback_query";
                } elseif (isset($data['message']['text'])) {
                    return "message";
                } elseif (isset($data['message']['photo'])) {
                    return "photo";
                } else {
                    return false;
                }
            } 

        http://skrinshoter.ru/s/260619/MYTzC8AP?a 
        Код кнопок оплаты
        
                    if(!$orderRaw['status']) {
                        // готовим кнопку для перехода в Яндекс.Деньги
                        $url = $this->getUrl($total, $user_id, $orderRaw['id']);
                        $buttons[] = [ $this->buildInlineKeyBoardButton('Оплатить через Яндекс.Деньги', '', $url),
        				$this->buildInlineKeyBoardButton(
        				    'При отправлении Наличными', 
        				    'getNal_' . $orderRaw['id']),
        				$this->buildInlineKeyBoardButton(
        				    'При отправлении Сканирование QR-Кода Яндекс.Деньги', 
        				    'getQrYandex_' . $orderRaw['id']),
        				];
                    } else {
                        // если заказ оплачен то уведомляем
                        $text .= "n<b>Заказ оплачен</b>n";
                    }
        

        У вас статья есть "Клавиатура как способ взаимодействия с ботом", не подскажете как вывести кнопки под сообщением по аналогии из той статьи после того как пользователь /start отправит?
        $justKeyboard = $this->getKeyBoard([[["text" => "Голосовать"], ["text" => "Помощь"]]]);
        A M 26.06.2019 в 14:03
  • Приветствую , статьи все крутые.
    Хотел спросить можно ли вместо этих кнопок , переделать все в клавиатуру? Я просто прям новичок в этом ) благодарю заранее за ответ)
    Ruslan Sharipov 08.05.2020 в 08:58
    • Переделать конечно можно, но будет не удобно (мое мнение)
      iMakeBots 11.05.2020 в 17:58
  • Рабочий скелет то!? Есть кто делал чат бота магазину по нему!?
    W O N D E R F U L 11.05.2020 в 06:23
    • Это бот больше для примера
      iMakeBots 11.05.2020 в 17:57
  • Здравствуйте! Пытаюсь запустить бота локально, указал в настройках свой id в телеграм, так же токен бота и данные для соединения с бд.
    Скачал и установил программу Windscribe для VPN соединения с локальным сайтом, но бот отказывается работать! А при запуске в консоли OpenServer файла index.php выдает ошибку:
    Warning: array_key_exists() expects parameter 2 to be array, null given in OSPaneldomainstelegrambot.locindex.php on line 71
    и
    Warning: array_key_exists() expects parameter 2 to be array, null given in OSPaneldomainstelegrambot.locindex.php on line 149
    Что я делаю не так? Может надо через setWebHook?
    Rus 27.05.2020 в 13:04
    • Этот бот работает через WebHook, то есть на него нужно настроить вебхук. Если вы не можете настроить webhook на локальный сервер, то можете воспользоваться NodeJS - у меня есть статья как это можно настроить.

      Ошибка array_key_exists() - отрабатывает нормально - так как скрипт ожидает данных от Телеграм и преобразовывает их в массив - далее передает в метод и там идет проверка array_key_exists() - а в вашем случае массив не передается - он и ругается.
      iMakeBots 27.05.2020 в 15:10
      • Я попробовал через ссылку в браузере активировать WebHook, но бот так и не ожил, ссылка имеет вид:
        https://api.tlgr.org/bot<TOKEN>/setWebHook?url=https://<my-site>/telegrambot/index.php
        Так же должно работать по идее? В боте пишу команду /start и /admin - ноль реакций...
        Rus 27.05.2020 в 23:02
      • Это одна сторона настройки.
        Теперь если вы переходите по адресу https://<my-site>/telegrambot/index.php - какой код ответа?
        iMakeBots 28.05.2020 в 11:59
        • В заголовках в мозиле на F12 жму, перехожу в консоль, а там - 500 Internal Server Error
          Rus 28.05.2020 в 12:16
        • Надо разобраться что за ошибка - посмотрите логи.
          Эту же ошибку получает вебхук когда пытается отправить данные в приложение.
          iMakeBots 28.05.2020 в 12:18
          • PHP Parse error: syntax error, unexpected '[' in /var/www/.../telegrambot/bot.php on line 139
            139 строка:
            // берем данные картинки
            $file_id = end($data['message']['photo'])['file_id'];
            странно, тут вроде все гут...
            Rus 28.05.2020 в 12:35
          • Какая версия PHP?
            iMakeBots 28.05.2020 в 12:37
            • 5.3.3
              Rus 28.05.2020 в 14:19
            • Поднимите до 7.2
              iMakeBots 28.05.2020 в 14:20
            • На сервере максимально доступная к сожалению только 7.0.10 :(
              Rus 28.05.2020 в 14:27
            • Попробуйте 7.0
              iMakeBots 28.05.2020 в 14:28
            • Посыпались ошибки:
              PHP Fatal error: Uncaught PDOException: SQLSTATE[42000] [1115] Unknown character set: 'utf8mb4' in /telegrambot/bot.php:2115
              #0 /telegrambot/bot.php(2115): PDO->__construct('mysql:host=loca...', 'database', 'pass', Array)
              #1 /telegrambot/bot.php(50): ShopBot->setPdo()
              #2 /telegrambot/bot.php(8): ShopBot->init()
              #3 {main}
              thrown in /telegrambot/bot.php on line 2115
              Да и сам сайт перестал работать... Откатил обратно на 5.3
              Ээх! Видно не судьба мне поюзать ваш скрипт)))
              Rus 28.05.2020 в 14:50
            • mysql не знаком с кодировкой utf8mb4
              iMakeBots 28.05.2020 в 15:07
  • Привет!
    Спасибо за ваш труд, еще не очень разобрался в коде, хочу спросить где и как лучше сделать такие вещи:
    1. Установить вторую цену товара (это может быть оптовая цена, или цена до скидки)
    2. Сделать, к примеру 2-3-уровневую систему подкатегорий товаров
    3. Установить кнопки "назад", "на главную" и "корзина" везде где их нет, просто для удобства
    4. Реализовать что-то типа истории заказов
    Спасибо!
    🧸 Винни Пух 05.06.2020 в 19:17
    • Вторую цену сначала надо добавить в бд (как столбец для хранения), запрашивать вы ее можете сразу же или перед или после первой цены, после обработки записывать в бд.

      Сделать неограниченную вложенность для категорий не сложно - нужно хранить id "родителя" для каждой записи категории. В категории 1 го уровня это будет 0

      Установить кнопки это еще проще - просто добавьте их при выводе сообщения там где вам необходимо

      История заказов есть в ЛК пользователя
      iMakeBots 09.06.2020 в 12:37
  • Подскажите как прикрутить агрегатор доставки для расчета ее стоимости и сроков, а также указание места выдачи скажем если товар отправояется DPD, или через почтамат pikpoint
    🇦 🇱 🇪 🇰 🇸 🇪 🇾 08.06.2020 в 11:16
    • Нужно добавить необходимые параметры к карточке товара - для расчета стоимости доставки товара (габариты, вес), также наверное необходимо знать точку отправки - делать запрос через API сервиса доставки - получить и вывести ответ в нужном месте в бот
      iMakeBots 09.06.2020 в 12:31
  • подскажите, как сюда добавить, чтобы админу присылался еще и состав заказа?

    $this->sendMsg([
                        'chat_id' => $this->admin, 
                        'text' => "Заказ #" . 
                            $this->getIdOrder() . " успешно оплачен. Сумма " . 
                            $this->getFullSum() . "."]);
    Грачик Абдулошвили 09.08.2020 в 21:41