Опубликовано 2 комментария

Telegram bot для WooCommerce. Часть 3.

Продолжаем писать Telegram bot для Woocommerce. В первой и второй части мы сделали каркас плагина, зарегистрировали бота, написали хуки для обработки регистраций чатов и многое другое. В этой части мы создадим метод, который будет отправлять сообщения о новых заказах в Телеграм. Получателями будут все те, кто подписан на нашего бота. Итак продолжим.

Каждый админ или менеджер магазина должен увидеть на странице плагина примерно вот такую картинку

Страница плагина WooCommerce Telegram
Страница плагина WooCommerce Telegram

Здесь мы видим токен и видим наш персональный ИД чата с ботом телеграм. Если у Вас что-то не так, то проверьте токен и название API метода в коде плагина. Давайте напишем метод который будет отправлять данные заказа на телеграмы всех админов и менеджеров кто зарегистрировался на странице плагина. Итак добавим немного кода в наш класс Telegram в файле include/telegram.php

add_action('woocommerce_checkout_order_processed',
  [$this, 'SendOrderToTelegram'], 10, 3);

public function SendOrderToTelegram($order_id, $posted, $order)
{
    $text = ' Заказ № ' . $order->get_order_number() . ' с сайта ' . get_site_url() . PHP_EOL;
    $text .= 'Клиент :' . $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() . PHP_EOL;
    $text .= 'Телефон :' . $order->get_billing_phone() . PHP_EOL;
    $text .= 'Email :' . $order->get_billing_email() . PHP_EOL;
    $text .= 'Сумма заказа :' . $order->get_total() . PHP_EOL;

    $text  .= 'Содержимое заказа :' . PHP_EOL;
    $items = $order->get_items();

    foreach ($items as $item) {
        $product = $item->get_product();
        $qty     = $item->get_quantity() ? $item->get_quantity() : 1;
        $price   = wc_format_localized_price($item->get_total() / $qty);
        $text    .= 'Товар :' . $product->get_name() . ' Кол-во :' . $qty . ' Цена :' . $price;
    }

    foreach ($this->getTelegramUsers() as $user) {
        $chatId = get_user_meta($user->ID, 'telegram', true);
        $this->sendMessageToTelegram($text, $chatId, $this->token);
    }
}

private function getTelegramUsers()
{
    return get_users([
      'meta_key' => 'telegram',
    ]);
}

Мы добавили в конструктор класса вызов action woocommerce_checkout_order_processed. Поместили в него название нашего метода SendOrderToTelegram. Обратите внимание, что мы указали число аргументов которое будет доступно в методе. В самом методе мы объявляем переменную $text в которую будем добавлять наше сообщение. Пробегаемся по позициям заказа и собираем минимальные данные которые хотим отправить в наш Telegram bot для WooCommerce.

Дальше нам нужно получить пользователей у которых заполнен мета тэг telegram . За это будет отвечать наш приватный метод getTelegramUsers. Воспользуемся встроенным в WordPress методом get_users. В качестве аргументов этот метод принимает в том числе и ключ meta_key. Это отберет только тех пользователей у которых в мета данных есть этот ключ.

Полученный от метода getTelegramUsers массив с данными мы переберем , и получим значение chatId из поля telegram. Теперь у нас есть все данные, чтобы отправить сообщение через нашего бота нужным пользователям. Воспользуемся ранее написанным методом sendMessageToTelegram и скормим ему $text $chatId и $token.

Итог.

Вот и все . Теперь наш Telegram bot для WooCommerce умеет отправлять данные заказа нужным пользователям системы. И выглядит это примерно так:

Пример оповещения о заказе отправленным Telegram
Пример оповещения о заказе

Я не стал заострять внимание на различных тонкостях, переводах плагина на другие языки и прочих мелочах. Я постарался донести основные вехи создания написания плагина WooCommerce для Telegram. Желающие могут модифицировать этот плагин под свои нужды, добавить различный функционал и тд.

В итоге наш код приобрел следующий вид.

<?php

if (!defined('ABSPATH')) {
    exit;
}

class Telegram extends WC_Integration
{

    const API_TELEGRAM = 'https://api.telegram.org/bot';

    public $registerWebhook;

    public $token;

    public $chatId;

    public $userId;

    public function __construct()
    {
        $current_user = wp_get_current_user();
        $this->userId = $current_user->ID;

        $this->id                 = "woo-telegram";
        $this->method_title       = "Телеграм бот для WooCommerce";
        $this->method_description = "Плагин связывает WooCommerce c Telegram ботом.";
        $this->init_form_fields();
        $this->init_settings();

        $this->token  = $this->settings['token'];
        $this->chatId = get_user_meta($this->userId, 'telegram', true);
        $this->registerWebhook = $this->checkExistsWebhook($this->token);

        add_action('woocommerce_api_woo-telegram',
          [$this, 'WooTelegramResponse']);
        add_action("woocommerce_update_options_integration_" . $this->id,
          [$this, "process_admin_options"]);
        add_action('woocommerce_checkout_order_processed',
          [$this, 'SendOrderToTelegram'], 10, 3);
    }

    public function init_form_fields()
    {
        $this->form_fields = [
          "token" => [
            "title"       => "Токен Telegram",
            "description" => "Введите token полученный от BotFather",
            "type"        => "text",
            "class"       => "tm-token",
            "desc_tip"    => true,
            "default"     => get_option("token"),
          ],
        ];
    }

    public function process_admin_options()
    {
        $result      = parent::process_admin_options();
        $this->token = $this->settings['token'];
        $this->setTelegramWebhook();
        $this->registerWebhook = $this->checkExistsWebhook($this->token);
        return $result;
    }

    function admin_options()
    {
        $hash = md5('telegram' . $this->userId);

        echo '<table class="form-table">';
        echo $this->generate_settings_html($this->form_fields, false);
        echo '</table>';
        if (!$this->registerWebhook) {
            echo '<a href="#" class="button-secondary" id="webhook">WebHook не зарегистрирован</a>';
        }
        if (empty($this->chatId)) {
            echo "<p> Найдите нашего бота @woo_telegram_bot нажмите Начать и напишите ему команду /key=$this->userId&auth=$hash долждитесь ответа бота и перегрузите эту страницу </p>";
        } else {
            echo "<p>Номер telegram чата : $this->chatId </p>";
        }
        $this->display_errors();
    }

    public function validate_text_field($key, $value)
    {
        if ($key == 'token') {
            if (!$this->checkToken($value)) {
                $this->add_error('Токен не существует');
            }
        }
        return parent::validate_text_field($key, $value);
    }

    private function setTelegramWebhook()
    {
        $logger = wc_get_logger();
        $url    = self::API_TELEGRAM . $this->token . '/setWebhook';
        $logger->info(wc_print_r($url, true));
        $args     = [
          'timeout'     => 5,
          'redirection' => 1,
          'httpversion' => '1.0',
          'blocking'    => true,
          'headers'     => ['Content-Type' => 'application/x-www-form-urlencoded'],
          'body'        => ['url' => home_url('/?wc-api=woo-telegram')],
        ];
        $response = wp_remote_post($url, $args);
        if (is_wp_error($response)) {
            $error_message = $response->get_error_message();
            $logger->info(wc_print_r($error_message, true));
        }
    }

    private function checkExistsWebhook(string $token)
    {
        $logger = wc_get_logger();
        $url    = self::API_TELEGRAM . $token . '/getWebhookInfo';
        $logger->info(wc_print_r($url, true));
        $response = wp_remote_get($url);
        $body     = wp_remote_retrieve_body($response);
        if (!empty($body)) {
            try {
                $data = json_decode($body, true);
                if (!empty($data['result']['url'])) {
                    return true;
                }
            } catch (Exception $e) {
            }
        }
        return false;
    }

    private function checkToken(string $token)
    {
        $url      = self::API_TELEGRAM . $token . '/getMe';
        $response = wp_remote_get($url);
        $body     = wp_remote_retrieve_body($response);
        if (!empty($body)) {
            try {
                $data = json_decode($body, true);
                if (!empty($data['result']['username'])) {
                    return true;
                }
            } catch (Exception $e) {
            }
        }
        return false;
    }

    public function WooTelegramResponse()
    {
        global $woocommerce;
        $data   = file_get_contents("php://input");
        $logger = wc_get_logger();
        try {
            $result = $this->decodePost($data);
            $userId = $this->parseText($result['text']);
            if (update_user_meta($userId, 'telegram', $result['chatId'])) {
                $eol  = PHP_EOL;
                $text = 'Добро пожаловать в WooCommerce.' . $eol;
                $text .= 'На странице плагина Вы должны увидеть номер Вашего чата ' . $result['chatId'] . $eol;
                $text .= 'Спасибо !';
                $this->sendMessageToTelegram($text, $result['chatId'],
                  $this->token);
            } else {
                if ($chatId = get_user_meta($userId, 'telegram', true)) {
                    $text = 'Вы уже зарегистрированы в WooCommerce.' . PHP_EOL;
                    $text .= 'Спасибо за то, что Вы с нами';
                    $this->sendMessageToTelegram($text, $chatId, $this->token);
                }
            }
        } catch (Exception $e) {
            $logger->info(wc_print_r($e->getMessage(), true));
        }
    }

    /**
     * @param string $text
     *
     * @return bool|mixed
     * @throws \Exception
     */
    private function parseText(string $text)
    {
        $input = [];
        parse_str($text, $input);
        $userId = empty($input['key']) ? false : $input['key'];
        $hash   = empty($input['auth']) ? false : $input['auth'];
        if ($userId && $hash && $hash == md5('telegram' . $userId)) {
            return $userId;
        }
        throw new Exception('Не найден пользователь или не совпал секрет !');
    }

    /**
     * @param string $post
     *
     * @return array
     * @throws \Exception
     */
    private function decodePost(string $post): array
    {
        $data   = json_decode($post, true);
        $text   = empty($data['message']['text']) ? false : $data['message']['text'];
        $text   = substr($text, 1);
        $chatId = empty($data['message']['chat']['id']) ? false : $data['message']['chat']['id'];
        if ($text && $chatId) {
            return [
              'text'   => $text,
              'chatId' => $chatId,
            ];
        }
        throw new Exception('Не хватает аргументов text или chatId');
    }


    public function sendMessageToTelegram(
      string $text,
      string $chatId,
      string $token
    ): void {
        $url      = self::API_TELEGRAM . $token . '/sendMessage';
        $args     = [
          'timeout'     => 5,
          'redirection' => 1,
          'httpversion' => '1.0',
          'blocking'    => true,
          'headers'     => ['Content-Type' => 'application/x-www-form-urlencoded'],
          'body'        => ['text' => $text, 'chat_id' => $chatId],
        ];
        $response = wp_remote_post($url, $args);
        $logger   = wc_get_logger();
        if (is_wp_error($response)) {
            $error_message = $response->get_error_message();
            $logger->info(wc_print_r($error_message, true));
        }
    }

    public function SendOrderToTelegram($order_id, $posted, $order)
    {
        $text = ' Заказ № ' . $order->get_order_number() . ' с сайта ' . get_site_url() . PHP_EOL;
        $text .= 'Клиент :' . $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() . PHP_EOL;
        $text .= 'Телефон :' . $order->get_billing_phone() . PHP_EOL;
        $text .= 'Email :' . $order->get_billing_email() . PHP_EOL;
        $text .= 'Сумма заказа :' . $order->get_total() . PHP_EOL;

        $text  .= 'Содержимое заказа :' . PHP_EOL;
        $items = $order->get_items();

        foreach ($items as $item) {
            $product = $item->get_product();
            $qty     = $item->get_quantity() ? $item->get_quantity() : 1;
            $price   = wc_format_localized_price($item->get_total() / $qty);
            $text    .= 'Товар :' . $product->get_name() . ' Кол-во :' . $qty . ' Цена :' . $price;
        }

        foreach ($this->getTelegramUsers() as $user) {
            $chatId = get_user_meta($user->ID, 'telegram', true);
            $this->sendMessageToTelegram($text, $chatId, $this->token);
        }
    }

    private function getTelegramUsers()
    {
        return get_users([
          'meta_key' => 'telegram',
        ]);
    }

}

Опубликовано 2 комментария

WooCommerce и Телеграм. Часть 2.

Итак продолжим писать наш плагин для WooCommerce и Телеграм. В первой части мы написали каркас плагина, зарегистрировали его в списке плагинов и добавили в интеграции WooCommerce.

Создаем бота в Telegram.

Теперь давайте создадим нашего бота. Для этого необходимо в Telegram найти отца всех ботов @BotFather и дать ему команду /newbot.

Переписка с BotFather
Переписка с BotFather

Добавляем поля в наш плагин.

Примерно в таком ключе пройдет Ваше общение с @BotFather в результате которого Вы получите token. Теперь этот токен необходимо сохранить в нашем плагине. Этим токеном мы будем подписывать наши сообщения боту. Давайте добавим наше первое поле в настройку плагина. После добавления наш класс Telegram в файле includes/telegram.php будет выглядеть так :

    public function __construct()
    {
        $this->id = "woo-telegram";
        $this->method_title = "Телеграм бот для WooCommerce";
        $this->method_description = "Плагин связывает WooCommerce c Telegram ботом.";
        $this->init_form_fields();
        $this->init_form_fields();
        $this->token = $this->settings['token'];
        add_action( "woocommerce_update_options_integration_" . $this->id, array( $this, "process_admin_options" ) );
    }

    public function init_form_fields(){
        $this->form_fields = [
            "token" => array(
                "title"       => "Токен Telegram",
                "description" => "Введите token полученный от BotFather",
                "type"        => "text",
                "desc_tip"    => true,
                "default"     => get_option( "token" )
            ),
        ];
    }

Мы добавили новый метод init_form_fileds в котором описали настройку необходимых нам полей , название, описание, тип, также добавили опцию по умолчанию . После того как мы сохраним наши настройки то заново зайдем на эту страницу и в это поле автоматически подставится сохраненное значение. Дальше мы определяем переменную token нашего класса Telegram и заполняем ее значением настройки ‘token’. После этого добавляем action «process_admin_options» который и сохранит все наши настройки в БД WooCommerce.

Теперь в Настройках WooCommerce у нас появилось новое поле Token которое мы можем заполнить и сохранить.

Токен для Telegram
Токен для Telegram

Пишем код плагина для связи с Telegram.

Давайте подумаем, что мы хотим получить от нашего бота ? Самое простое это получать уведомление когда кто-то из клиентов совершает заказ. Это и будет наше минимальное ТЗ.

Но сначала давайте подружим WooCommerce и Телеграм. Все общение с Телеграм может происходить двумя способами. 1) Мы сами запрашиваем обновления. 2) Телеграм присылает нам уведомления на указанный нами адрес. Мы выберем способ номер 2). Для этого у нашего сайта должен быть установлен SSL сертификат. Чтобы наш бот мог отправлять нам сообщения, нам надо получить ИД чата между телеграм ботом и нашим персональным telegram. Для этого мы будем отсылать нашему боту в телеграме команду вроде /key=ИД_юзера_на_сайте&auth=ХЭШ_для_защиты. Наш сайт будет принимать от бота ИД чата и прописывать его нужному юзеру.

Добавим в конструктор __construct() нашего класса вызов action, добавим сам метод WooTelegramResponse, пару вспомогательных методов и так же напишем метод отправки сообщений в Telegram.

add_action('woocommerce_api_woo-telegram',[$this,'WooTelegramResponse']);

public function WooTelegramResponse()
{
    global $woocommerce;
    $data   = file_get_contents("php://input");
    $logger = wc_get_logger();
    try {
        $result = $this->decodePost($data);
        $userId = $this->parseText($result['text']);
        if (update_user_meta($userId, 'telegram', $result['chatId'])) {
            $text = 'Добро пожаловать в WooCommerce.' . PHP_EOL;
            $text .= 'На странице плагина Вы должны увидеть номер Вашего чата ' . $result['chatId'] . PHP_EOL;
            $text .= 'Спасибо !';
            $this->sendMessageToTelegram($text, $result['chatId'], $this->token);
        } else {
            if ($chatId = get_user_meta($userId, 'telegram', true)) {
                $text = 'Вы уже зарегистрированы в WooCommerce.' . PHP_EOL;
                $text .= 'Спасибо за то, что Вы с нами';
                $this->sendMessageToTelegram($text, $chatId, $this->token);
            }
        }
    } catch (Exception $e) {
        $logger->info(wc_print_r($e->getMessage(), true));
    }
}

private function parseText(string $text)
{
    $input = [];
    parse_str($text, $input);
    $userId = empty($input['key']) ? false : $input['key'];
    $hash   = empty($input['auth']) ? false : $input['auth'];
    if ($userId && $hash && $hash == md5('telegram2019' . $userId)){
        return $userId;
    }
    throw new Exception('Не найден пользователь или не совпал секрет !');
}

private function decodePost(string $post): array
{
    $data   = json_decode($post, true);
    $text   = empty($data['message']['text']) ? false : $data['message']['text'];
    $text   = substr($text, 1);
    $chatId = empty($data['message']['chat']['id']) ? false : $data['message']['chat']['id'];
    if ($text && $chatId) {
        return [
          'text'   => $text,
          'chatId' => $chatId,
        ];
    }
    throw new Exception('Не хватает аргументов text или chatId');
}

public function sendMessageToTelegram(
  string $text,
  string $chatId,
  string $token
): void {
    $url      = 'https://api.telegram.org/bot' . $token . '/sendMessage';
    $args     = [
      'timeout'     => 5,
      'blocking'    => true,
      'headers'     => ['Content-Type' => 'application/x-www-form-urlencoded'],
      'body'        => ['text' => $text, 'chat_id' => $chatId],
    ];
    $response = wp_remote_post($url, $args);
    $logger   = wc_get_logger();
    if (is_wp_error($response)) {
        $error_message = $response->get_error_message();
        $logger->info(wc_print_r($error_message, true));
    }
}

Добавив action woocommerce_api_woo-telegram мы тем самым задекларировали, что у нас по адресу https://наш_сайт.ru/?wc-api=woo-telegram будет находится наш обработчик WooTelegramResponse. Смысл этого обработчика в том, что он принимает данные с Telegram декодирует их, обрабатывает и записывает нужному юзеру ИД чата с Телеграм.

Добавляем необходимую информацию на страницу плагина.

Итак у нас готов обработчик, теперь надо сообщить Telegram куда он должен отсылать всю ту информацию которую он получит в чате. Но перед этим сделаем проверку на правильность введенного Токена. Переопределим встроенный метод validate_text_field и используем его для проверки нашего токена.

public function validate_text_field($key, $value)
{
    if ($key == 'token') {
        if (!$this->checkToken($value)) {
            $this->add_error('Токен не существует');
        }
    }
    return parent::validate_text_field($key, $value);
}

Здесь мы смотрим на полученный ключ сравниваем его с тем что мы задали в методе init_form_fields и запускаем проверку с помощью метода checkToken, если метод вернет false то добавим ошибку в список ошибок и дальше отдадим управление родительскому методу. Опишем метод checkToken :

private function checkToken(string $token)
{
    $url      = 'https://api.telegram.org/bot' . $token . '/getMe';
    $response = wp_remote_get($url);
    $body     = wp_remote_retrieve_body($response);
    if (!empty($body)) {
        try {
            $data = json_decode($body, true);
            if (!empty($data['result']['username'])) {
                return true;
            }
        } catch (Exception $e) {
        }
    }
    return false;
}

В Telegram есть простой метод getMe который возвращает данные бота или 404 ошибку если бота с таким токеном не существует. Если мы получаем от Телеграм username в данных, то считаем что проверка прошла и возвращаем true, во всех остальных случаях возвращаем false.

Теперь у нас есть валидация Token напишем метод setTelegramWebhook который указывает Telegram куда отсылать webhook.

private function setTelegramWebhook()
{
    $logger = wc_get_logger();
    $url    = 'https://api.telegram.org/bot' . $this->token . '/setWebhook';
    $logger->info(wc_print_r($url, true));
    $args     = [
      'timeout'     => 5,
      'redirection' => 1,
      'httpversion' => '1.0',
      'blocking'    => true,
      'headers'     => ['Content-Type' => 'application/x-www-form-urlencoded'],
      'body'        => ['url' => home_url('/?wc-api=woo-telegram')],
    ];
    $response = wp_remote_post($url, $args);
    if (is_wp_error($response)) {
        $error_message = $response->get_error_message();
        $logger->info(wc_print_r($error_message, true));
    }
}

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

Проведем еще немного рефакторинга и добавим проверок в результате которых наш код примет примерно такой вид.

<?php

if (!defined('ABSPATH')) {
    exit;
}

class Telegram extends WC_Integration
{

    const API_TELEGRAM = 'https://api.telegram.org/bot';

    public $registerWebhook;

    public $token;

    public $chatId;

    public $userId;

    public function __construct()
    {
        $current_user = wp_get_current_user();
        $this->userId = $current_user->ID;

        $this->id                 = "woo-telegram";
        $this->method_title       = "Телеграм бот для WooCommerce";
        $this->method_description = "Плагин связывает WooCommerce c Telegram ботом.";
        $this->init_form_fields();
        $this->init_settings();

        $this->token  = $this->settings['token'];
        $this->chatId = get_user_meta($this->userId, 'telegram', true);
        $this->registerWebhook = $this->checkExistsWebhook($this->token);
        add_action('woocommerce_api_woo-telegram',
          [$this, 'WooTelegramResponse']);
        add_action("woocommerce_update_options_integration_" . $this->id,
          [$this, "process_admin_options"]);
    }

    public function init_form_fields()
    {
        $this->form_fields = [
          "token" => [
            "title"       => "Токен Telegram",
            "description" => "Введите token полученный от BotFather",
            "type"        => "text",
            "class"       => "tm-token",
            "desc_tip"    => true,
            "default"     => get_option("token"),
          ],
        ];
    }

    public function process_admin_options()
    {
        $result      = parent::process_admin_options();
        $this->token = $this->settings['token'];
        $this->setTelegramWebhook();
        $this->registerWebhook = $this->checkExistsWebhook($this->token);
        return $result;
    }

    function admin_options()
    {
        $hash = md5('telegram2019' . $this->userId);

        echo '<table class="form-table">';
        echo $this->generate_settings_html($this->form_fields, false);
        echo '</table>';
        if (!$this->registerWebhook) {
            echo '<a href="#" class="button-secondary" id="webhook">WebHook не зарегистрирован</a>';
        }
        if (empty($this->chatId)) {
            echo "<p> Найдите нашего бота @woo_telegram_bot нажмите Начать и напишите ему команду /key=$this->userId&auth=$hash долждитесь ответа бота и перегрузите эту страницу </p>";
        } else {
            echo "<p>Номер telegram чата : $this->chatId </p>";
        }
        $this->display_errors();
    }

    public function validate_text_field($key, $value)
    {
        if ($key == 'token') {
            if (!$this->checkToken($value)) {
                $this->add_error('Токен не существует');
            }
        }
        return parent::validate_text_field($key, $value);
    }

    private function setTelegramWebhook()
    {
        $logger = wc_get_logger();
        $url    = self::API_TELEGRAM . $this->token . '/setWebhook';
        $logger->info(wc_print_r($url, true));
        $args     = [
          'timeout'     => 5,
          'redirection' => 1,
          'httpversion' => '1.0',
          'blocking'    => true,
          'headers'     => ['Content-Type' => 'application/x-www-form-urlencoded'],
          'body'        => ['url' => home_url('/?wc-api=woo-telegram')],
        ];
        $response = wp_remote_post($url, $args);
        if (is_wp_error($response)) {
            $error_message = $response->get_error_message();
            $logger->info(wc_print_r($error_message, true));
        }
    }

    private function checkExistsWebhook(string $token)
    {
        $logger = wc_get_logger();
        $url    = self::API_TELEGRAM . $token . '/getWebhookInfo';
        $logger->info(wc_print_r($url, true));
        $response = wp_remote_get($url);
        $body     = wp_remote_retrieve_body($response);
        if (!empty($body)) {
            try {
                $data = json_decode($body, true);
                if (!empty($data['result']['url'])) {
                    return true;
                }
            } catch (Exception $e) {
            }
        }
        return false;
    }

    private function checkToken(string $token)
    {
        $url      = self::API_TELEGRAM . $token . '/getMe';
        $response = wp_remote_get($url);
        $body     = wp_remote_retrieve_body($response);
        if (!empty($body)) {
            try {
                $data = json_decode($body, true);
                if (!empty($data['result']['username'])) {
                    return true;
                }
            } catch (Exception $e) {
            }
        }
        return false;
    }

    public function WooTelegramResponse()
    {
        global $woocommerce;
        $data   = file_get_contents("php://input");
        $logger = wc_get_logger();
        try {
            $result = $this->decodePost($data);
            $userId = $this->parseText($result['text']);
            if (update_user_meta($userId, 'telegram', $result['chatId'])) {
                $eol  = PHP_EOL;
                $text = 'Добро пожаловать в WooCommerce.' . $eol;
                $text .= 'На странице плагина Вы должны увидеть номер Вашего чата ' . $result['chatId'] . $eol;
                $text .= 'Спасибо !';
                $this->sendMessageToTelegram($text, $result['chatId'],
                  $this->token);
            } else {
                if ($chatId = get_user_meta($userId, 'telegram', true)) {
                    $text = 'Вы уже зарегистрированы в WooCommerce.' . PHP_EOL;
                    $text .= 'Спасибо за то, что Вы с нами';
                    $this->sendMessageToTelegram($text, $chatId, $this->token);
                }
            }
        } catch (Exception $e) {
            $logger->info(wc_print_r($e->getMessage(), true));
        }
    }

    /**
     * @param string $text
     *
     * @return bool|mixed
     * @throws \Exception
     */
    private function parseText(string $text)
    {
        $input = [];
        parse_str($text, $input);
        $userId = empty($input['key']) ? false : $input['key'];
        $hash   = empty($input['auth']) ? false : $input['auth'];
        if ($userId && $hash && $hash == md5('telegram2019' . $userId)) {
            return $userId;
        }
        throw new Exception('Не найден пользователь или не совпал секрет !');
    }

    /**
     * @param string $post
     *
     * @return array
     * @throws \Exception
     */
    private function decodePost(string $post): array
    {
        $data   = json_decode($post, true);
        $text   = empty($data['message']['text']) ? false : $data['message']['text'];
        $text   = substr($text, 1);
        $chatId = empty($data['message']['chat']['id']) ? false : $data['message']['chat']['id'];
        if ($text && $chatId) {
            return [
              'text'   => $text,
              'chatId' => $chatId,
            ];
        }
        throw new Exception('Не хватает аргументов text или chatId');
    }


    public function sendMessageToTelegram(
      string $text,
      string $chatId,
      string $token
    ): void {
        $url      = self::API_TELEGRAM . $token . '/sendMessage';
        $args     = [
          'timeout'     => 5,
          'redirection' => 1,
          'httpversion' => '1.0',
          'blocking'    => true,
          'headers'     => ['Content-Type' => 'application/x-www-form-urlencoded'],
          'body'        => ['text' => $text, 'chat_id' => $chatId],
        ];
        $response = wp_remote_post($url, $args);
        $logger   = wc_get_logger();
        if (is_wp_error($response)) {
            $error_message = $response->get_error_message();
            $logger->info(wc_print_r($error_message, true));
        }
    }

}

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

Опубликовано Оставить комментарий

Telegram бот для WooCommerce. Часть 1.

Этой статьей я открываю небольшой тренинг по написанию расширений для WooCommerce. Чтобы не писать зря мы сделаем Telegram бот для WooCommerce.

Bot father
Так выглядит папа ботов

Давно хотел написать Telegram бота для своего Интернет магазина.Я хочу получать всю информацию о заказах в свой telegram и может быть реализовать какие то функции по управлению заказами.

Вначале статьи приведу несколько ссылок на официальную документацию :

От слов к делу. Создадим начальный каркас нашего плагина. Откроем PHPStorm или любой другой привычный Вам редактор PHP , создадим файл woo-telegram.php и заполним его.

Вот так это может выглядеть :

/*
Plugin Name: WooCommerce -> Telegram
Description: Плагин интеграции  WooCommerce c Telegram.
*/
if ( ! defined('ABSPATH')) {
    exit;
}

if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
    function wc_telegram_add_integration($integrations) {
        global $woocommerce;

        if (is_object($woocommerce)) {
            include_once( 'includes/telegram.php' );
            $integrations[] = 'Telegram';
        }
        return $integrations;
    }

    add_filter('woocommerce_integrations', 'wc_telegram_add_integration', 10);
}

Вначале идут описание плагина, стандартные проверки на прямой вызов скрипта и существование плагина WooCommerce. Дальше пишем функцию wc_telegram_add_integration смысл которой создать нашу интеграцию в WooCommerce. В ней же определяем где будет лежать наш основной код (includes/telegram.php). Завершаем все стандартным фильтром add_filter который регистрирует нашу функцию в WooCommerce.

Вид плагина
Так выглядит наш плагин в общем списке плагинов

В принципе наш вновь созданный Telegram бот для WooCommerce уже имеет вид плагина. Разместив его в папке wp-content/plugins вы увидите его в списке плагинов и даже сможете активировать/деактивировать его.

Но в WooCommerce->Настройки->Интеграции Вы его пока не увидите. Исправим это и напишем минимальную обвязку плагина. Открываем наш файл includes/telegram.php и добавляем в него:

<?php

if (!defined('ABSPATH')) {
    exit;
}

class Telegram extends WC_Integration
{
    public function __construct()
    {
        $this->id = "woo-telegram";
        $this->method_title = "Телеграм бот для WooCommerce";
        $this->method_description = "Плагин связывает WooCommerce c Telegram ботом.";
    }
}

В начале опять проверка на прямой запуск плагина, дальше мы создаем наш класс Telegram и наследуем его от встроенного класса WC_Integration. Все плагины для WooCommerce интеграций должны наследоваться от этого класса. Заполняем минимально необходимой информацией. Где id это идентификатор нашего класса, method_title — Наименование которое будет отображаться в настройках а method_description описание плагина. Оно также будет отображаться в настройках плагина.

Интеграции в Woocommerce
Интеграции в Woocommerce

Активируйте плагин , перейдите на страницу Интеграция настроек WooCommerce и Вы увидите , что наш плагин уже существует и отображает всю информацию которую мы в него пока заложили. На этом закончим первую часть, в следующей части мы зарегистрируем нашего бота в телеграм, добавим в него настроек и подумаем над минимальным функционалом.

Спасибо.

Опубликовано Оставить комментарий

Плагин Google Chart на PlatformaLP

Platforma LP и Google Chart

В своей работе я часто использую платформу platforma lp  для быстрого создания лэндингов и тестирования различных гипотез. И вот решили мы в сервисе https://lead2call.ru немного оживить лэндинг и добавить ему интерактивности. Для мы выбрали google chart графики которые в режиме онлайн отображали бы интересные данные для потенциальных клиентов. И вот, что получилось!

lead2call

Что для этого потребовалось :

1) Библиотека  Google chart, для отображения данных

2) API на стороне backend , которое бы отдавало нужные нам данные

3) Cкрипт на platforma lp

 

 

С первыми двумя пунктами все просто библиотека google chart в свободном доступе, нужные Вам данные вы либо берете с  API сайта или с любого другого места, остановимся подробнее на том как все это сделать в platforma lp.

Для размещения произвольного Javascript кода на странице лэндинга Вам нужно будет создать плагин, добавить его Вы можете на в настройках страницы в меню метрика и скрипты. Дальше в самом лэндинге Вы вставляете произвольные HTML блоки с нужными вам Тэгами , как пример : <div id=»chart1″></div>

 

Дальше все просто , пишем скрипт, подгружающий по Ajax данные с нашего API , скармливаем  google полученные данные и выводим их с помощью функций в нужный нам div, пример :

var dataForNetwork = google.visualization.arrayToDataTable(data[‘network’]);  — инициализируем переменную и заполняем ее данными получеными с API

var options = { backgroundColor: ‘black’, legend: {position: ‘none’}, animation: { duration: 2000, easing: ‘out’, startup: true,}, title: ‘Пример вывода графика Google’, titleTextStyle: {color: ‘white’}, ‘height’: 300,}  — инициализируем переменную содержащую различные опции для отображения графика и добавляем немного анимации.

var chartNetwork = new google.visualization.ColumnChart(document.getElementById(‘chart1’)); — инициализируем переменную содержащую нужный нам вид графика и указываем в каком DIV будем отображат его

chartNetwork.draw(dataForNetwork, options); — ну и выводим все это на страницу .

Вот и все !

 

p.s.  Я оставил за бортом описание подключения самих графиков и написание ajax запроса , если это интересно пишите в комментариях я расскажу как это сделал я.

Опубликовано Оставить комментарий

SEO для сайта автозапчастей (часть 2)

Выбор инструментов

Итак определившись с выбором CMS, настало время продумать SEO для сайта автозапчастей. Заказ пришел ко мне из далекого города Челябинск, поэтому вооружившись wordstat , yandex и keycollector приступим.

SEO для сайта автозапчастей - keycollector

Заказ был на разработку интернет магазина автозапчастей для Рено Логан обоих поколений , вот и запустим кейколектор прочесывать все возможные варианты. Выбираем нужный регион в настройках Яндекса, не будим пока мудрить и что-то выдумывать, вбиваем Рено логан в список и запускаем KeyCollector парсить левую колонку яндекс Wordstat.  Пока он будет заниматься вытаскиваем всего того, что там люди ищут в Яндексе со словосочетанием Рено Логан я изучаю конкурентов на этом поле.

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

Постановка гипотезы

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

SEO для сайта автозапчастей - Низкочастотные запросы Поработав Keycollector выдал мне примерно 2000 запросов со словосочетанием Рено Логан в этом регионе. Проанализировав их я убедился в верности своей гипотезы и попутно выявил еще несколько интересных моментов. Всего можно всю выдачу яндекса разбить на три большие части :

  1. Выдача по самому автомобилю, пользователи интересуются характеристиками, ценой , скидками, кредитами и остальным подобным связанным именно с этим автомобилем. Эти запросы не являются первоочередными для нашего сайта , мы же не собираемся продавать Рено Логан )
  2. Выдача по поиску различных запасных частей, это как раз то , что нам и требуется. Наши самые Важные ключевые слова по которым мы и будем стараться угодить господам из Яндекса и Гугла
  3. Выдача по решению различного рода проблем связанных с ремонтом данного автомобиля, вот этот момент изначально я упускал из виду, но на самом деле у него очень хорошая низкочастотная база и мне нужно будет продумать где и как можно будет учесть это при разработке сайта. Ремонт автомобиля это конечно совсем не то направление в котором работает заказчик, но как мне кажется оно может способствовать повышению конверсии магазина.

Итак, кейколлектор выдал мне обильную пищу для составления семантического ядра магазина. Займусь каталогизацией и примеркой полученных запросов к CMS. Что из этого получится читайте далее….

Опубликовано Оставить комментарий

Создание сайта автозапчастей на платформе YUPE часть 1

Пару недель назад поступила ко мне заявка из региона на создание сайта автозапчастей для иномарок, точнее для одной конкретной модели. Так как тема запчастей мне близка, плюс я давно хотел сделать узконаправленный интернет магазин запчастей для какой нибудь марки автомобиля и применить полученные мной за последнее время знания в области SEO, я согласился.

Как показывает мой опыт работы с ИМ запчастей , есть два пути по которым идет развитие таких интернет магазинов.

  1. Мультибренд — когда магазин занимается запчастями ко всем автомобилям
  2. Узкая специализация — когда магазин занимается исключительно одной маркой автомобиля или одной моделью, или даже один брендом.

У обоих вариантов есть и плюсы и минусы, но не стоит их рассматривать как истину в последней инстанции. Итак :

Плюсы первого варианта.

Широкий ассортимент автозапчастей потенциально должен привлечь более широкую аудиторию, а значит большее количество заказов.

Минусы первого варианта.

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

Плюсы второго варианта.

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

Минусы второго варианта.

Потенциально более меньшая аудитория, а  значит и посещаемость ИМ, что влияет на количество заказов, а значит и показатели выручки и прибыли магазина.

 

Почему то развитие 80-90 % всех интернет магазинов запасных частей идет по первому пути. Отчасти этому способствовала слава крупнейшего ИМ запчастей в рунете Exist.ru , отчасти общее состояние этой отрасли, но только несколько игроков сумели составить конкуренцию этому монстру и войти когорту лидеров.

Тем не менее 10-20% интернет магазинов заняли свои узкие ниши и неплохо себя чувствуют.

Именно этот тип магазина я и хочу создать.

Итак  с типом магазина я определился. Создание сайта автозапчастей потребует выбрать платформу или CMS, когда речь идет об узкой нише, то редко количество товаров превышает 1000-10000 SKU, а значит принципиально можно выбирать любую платформу или CMS из топ-10, дело только в удобстве использования, оформлении и всяких фишках/наворотах. Но для начала пробежимся по имеющимся вариантам уже заточенным под торговлю автозапчастями.

Проанализировав различные варианты я остановился на CMS Yupe , почему именно эта платформа , а не WordPress или Битрикс ? Все просто, yupe из коробки имеет множество модулей, она написана на любимом мной фреймворке  YII, да и просто захотелось пройти весь цикл создания ИМ на этой платформе для более глубокого ее изучения.

Продолжение следует…..

Опубликовано Оставить комментарий

Проблема выбора запасных частей

Обзор рынка запчастей

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

На этом этапе результат во многом зависит от мнения так называемых “знатаков”, навязывающих нам свое мнение. Для кого-то это лучший друг-механик, для кого-то — продавец магазина, для кого-то — интренет. Как правило, ключевая фраза, жирной чертой подводящая к принятию окончательного решения, это: “Я на свою машину ставлю …..”. И мы верим….Слепо верим. А что нам остается…?

Цель же данного опуса не рекламировать бренды установленные на мой автомобиль, а рассказать о рынке запасных частей каков он есть, используя мой 12 летний опыт “знатока”, изо дня в день “впаривающий” Вам запчасти .

Опыт работы

Далекий 2006 год, я работаю у официально дилера Ford. Всем клиентам, обращающимся с вопросом приобретения запасных частей, я говорю магическое слово ОРИГИНАЛ. Самое забавное — это то, что я сам свято верю в эту “ панацею”. Тогда доллар был другим, да и рынок аналогов был не таким красочным как сейчас, поэтому формула — “нет ничего лучше оригинала” работала на все сто.

2008 год. Работа в крупном сетевом мультибрендовом магазине. Я, не подозревая, сам себе открываю “Америку”. Выясняется, что оригинал это тот же неоригинал, но только произведен по заказу производителя автомобилей на заводе производителя запасных частей (сорян за тавтологию), с логотипом автомобильного бренда. Например, бренд Sachs выпускает сцепление, одно под своим брендом, второе по заказу, например Форда (уже с логотипом последнего).

Сцепление sachs
Сцепление sachs

Сцепление sachs произведенное для форд
Сцепление sachs произведенное для форд

 

И так многие производители входящие в когорту со звучным названием “Оригинальные Поставщики”. Естественно нарастает волна негодования интернет сообщества: “Да как они смеют! Запчасти дороже в два раза, а качество тоже!”. Резонно. НО! Контроль качества при заказе автопроизводителя строже, а значит и качество должно быть лучше. Тут в пору добавить о другой форме существования запасных частей на рынке под общим брендом “Контрафакт”. Именно встреча с ним рождает мифы о гадком качестве неоригинальных запчастей.

Ну и наценка в легковом на бренд не велика. Вот в грузовом — это да. Ниже — таблица нескольких запчастей для Scania. Неоригинал ни в коем случае не “завод у дяди Ашота”, а серьезные европейские производители.

Артикул Наименование Цена , оригинал Цена , неоригинал
1752963 муфта 80890 руб 7791 руб
1495269 диск муфта 82599 руб 5135 руб
2190276 шестерня 74486 руб 19387 руб

 

В результате, в 2008-2015гг. работа в разных оптовых и розничных направлениях запасных частей для иномарок только укрепляла меня во мнении, что лучше брать аналог хороших брендов, чем оригинал, по, наверное, 90-та процентов позиций.

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

Картинка с затертым фордом
на картинке затерт номер форд

Но последний кризис все изменил.

2015-2017гг. я работаю в компании импортирующей запасные части из Китая в РФ. Как выясняется, на заводах Китая запчасти имеют следующую градацию: “качество в оригинал”, “среднее качество” и “откровенная кака”.

По всей видимости, это понимают и производители автомобилей. Думаю, многие догадываются, что у производителей автомобилей есть определенный бюджет на запчасти, после того как они выпустят авто. Первые три года существования модели на рынке он довольно большой, что позволяет им размещать заказ на заводы топовых производителей Bosch, Sachs, Behr и т.п., далее бюджет снижается и заказ оригинальных запчастей размещается на заводы менее раскрученных бренды ERA, Febi и т.п. Ну и апогей — они поняли, что можно размещать заказы на заводы Китайских брендов, благо и качество у них подросло и не дорого.

И как результат проблема выбора запчастей только усугубляется, например, в оригинале в VW фильтра стали фирмы UFI, фирма не плоха и штаб квартира у нее в Италии, и работают они с 1972 года. Но пугающий всех покупателей запчастей момент остается — произведено в Китае. Да и цены у них были в прошлом дешевле, но как только стали делать для VW group, сразу ценники подросли))).

Еще Вам история для раздумий.

Товарищ купил в прошлом месяце Ауди А6 новую из салона. Машина стоит порядка 3 млн. рублей. Стекла в оригинале стоят FUYAO Китай, который стоит дешево и многими людьми даже не рассматривался к покупке.

Стекло FUYAO на новом автомобиле Ауди
Стекло FUYAO на новом автомобиле Ауди

Официальный представитель FUYAO в Челябинске
Официальный представитель FUYAO в Челябинске

Официальный представитель FUYAO в Челябинске - склад
Официальный представитель FUYAO в Челябинске — склад

Я это не к тому, что, ай, Ауди какие обманщики, а к тому, что возникает противоречие.

10 лет назад нас приучали что неоригинал — это плохо, затем, не европейские бренды это плохо, но вот прошло время и пора нам вновь менять свои взгляды….

Всем любви и удачи в выборе.

Опубликовано Оставить комментарий

Часть вторая «сайт тормозит» или кэш в YII1

Как использовать кэш в YII1 .

Итак в первой части я рассказывал о борьбе с медленным сайтом клиента и использовании программы XDEBUG. Сайт клиента до начала работы выдавал умопомрачительные 6-7секунд времени до загрузки первого байта и с помощью XDEBUG удалось найти и локализовать проблему. Время отдачи первого байта упало до приемлимых на мой  взгляд 800-900мс, но клиенту этого показалось мало и он решил вывести время  ответа сайта по основным своим страницам каталога в район 200-300мс, а значит можно попробовать использовать кэш в YII1 .

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

Контроллер YII1
Типичный контроллер YII1

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

 

 

Кэш в YII1

Изучив нужные мне view-шки на предмет кода решаю внедрить примерно одинаковые конструкции отличющиеся только разными зависимостями и названиями ИД кэшей.

if($this->beginCache('car'.$model->id.'type'.$type.'page'.$page, array('duration'=>60*60*24*365,'dependency'=>array(
'class'=>'system.caching.dependencies.CDbCacheDependency',
'sql'=>'SELECT update_at FROM katalog_vavto_items WHERE cathegory_id=:cathegory_id order by update_at desc limit 1',
'params'=>array(':cathegory_id'=>$model->id),
)))) {
// здесь какой то контент который нужно закэшировать
$this->endCache(); }

Если вкратце, то кэшируем разные страницы модели и ее типов  на год ( ‘duration’=>60*60*24*365), а вторым условием использования кэша делаем проверку на изменение или добавление какой либо карточки принадлежащей модели (атрибут update_at который изменится если вдруг в какую то карточку внесут изменения или добавят новую ).

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

Бинго !
 

Опубликовано Оставить комментарий

Как я искал проблему «сайт тормозит» !

XDEBUG result

Поступила мне тут на днях следующая задача, помочь решить проблему «сайт тормозит».

Краткое предисловие было в том, что время открытия заглавной страницы сайта составляло порядка 6-8 секунд , по словам клиента » сайт тормозит» ! При всем этом, ничего такого особенного и тяжеловесного на сайте нету. Задача интересная и не совсем обычная для меня и я решил взяться.

Сайт написан на  YII который славится своей быстротой. Первичное исследование железа на котором установлен сайт показало VPS со следующими параметрами : 8 ядерный процессор, 32Гб ОЗУ, SSD диск . Причина скорее всего не в железе. Сайт работает под управлением PHP56 и NGINX в качестве СУБД выступает MYSQL 5.5. На всякий пожарный смотрим настройки mysql (а то тут был у меня один случай 🙂 , все в порядке база на innoDB. Все размеры буферов и кэшей соответствуют размерам БД, да и обычный show processlist  ни дает ничего криминального, как иногда это бывает.

Вывод chrome

Запускаем chrome посмотреть выдачу , показатель TTFB то есть время через которое браузер получил первый байт составляет 6.23 сек ! Причем повторные загрузки не изменили картины. При всем этом размер страницы всего 65Кб.

 

Проблема «сайт тормозит» явно в коде сайта !

 

 

Как спрофилировать код PHP ?

Проблема локализована, но не понятно, что именно так тормозит. Для того, чтобы это понять, я устанавливаю и запускаю XDEBUG. Включаем срабатывание xdebug по триггеру  и настраиваем директорию куда он будет писать свой файл. Запускаем обновление главной страницы с параметром ?XDEBUG_PROFILE и любуемся файлом в 97MB )))

Для быстрого анализа файла выдачи Xdebug  есть офигенский инструмент webgrind. Устанавливаем его с GITHUB репозитория и скармливаем ему наш 97Мб файлик. Убираем функции PHP и сортируем по Total Self Cost для начала. Это самый затратный процесс.

На первом месте вызов KatalogVavtoModule->generateItemsPathsMap. Самое частое кто его вызывает этоKatalogAvtoUrlRule->createUrl. Заглядываем внутрь, а там 1,5 млн. выполнений операции array_key_exists !!! Ныряем в код и проводим небольшое расследование. Итого: модуль KatalogAvtoUrlRule->createUrl переопределяет собственные методы CBaseUrlRule createUrl и parseUrl , для формирования ссылок на товары и категории товаров, в которых идет обращение к модулю KatalogVavtoModule и методу generateItemsPathsMap. Идем дальше в этот метод и видим следующую картину. Каждый раз при формировании ссылок выполняется поиск и выборка из БД  более чем 11 тыс. строк, обработка и формирование карты ссылок которая затем скармливается createUrl и уже по этой карте формируется нужная ссылка.

На главной странице примерно 132 ссылки на различные категории и товары, 132 х 11 тысяч строк БД + операции проверки и формирования нужных ссылок. Вот то, что тормозило сайт ! Так как товары практически не меняются^ посещаемость сайта не велика и дабы не углубляться в дебри бизнес-логики, включаю кэширование и дописываю код. Теперь карта ссылок будет хранится в CFileCache и при всевозможных изменениях в категориях или товарах. Заново перестраиваться. Сказано сделано.

Стало 893ms !!!  Победа !!!

 

 

Пишите в коментариях как Вы справлялись с похожими проблемами.