javascript

Как бесплатный заказ на Авито превратился в задачу на 2,5 месяца

  • воскресенье, 7 июня 2026 г. в 00:00:08
https://habr.com/ru/articles/1042334/

Полгода назад я узнал о крутом инструменте — Apps Script. Это расширение Google для их приложений, например, Таблицы, Документы, Презентации и т. д. Сначала при помощи этого инструмента мне удалось для спортивной школы автоматизировать выгрузку информации из CRM в Google таблицу и настроить отчеты для работы с этой информацией, а потом, когда распробовал инструмент на вкус, сделал личного Telegram-бота. Обо всем этом и пойдет дальше речь.

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

Оглавление:

  1. Мой первый заказ на Авито

  2. Без внятного ТЗ — результат ХЗ

  3. Что находится под капотом решения?

  4. Продолжение истории. Что было дальше?

  5. Бойтесь своих желаний

В разделах 1, 2 рассказываю о том, как я нашел себе приключений на пятую точку и как из них выкручивался. В разделе 3 представлена архитектура решения, которая получилась, и приведены куски кода. Раздел 4 небольшой, там речь пойдет про эксперимент, который появился благодаря событиям из разделов 1-3, а в финальном разделе 5 подводятся итоги.

Предлагаю начать!

Мой первый заказ на Авито

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

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

Так я оказался на Авито. Там разместил объявление в духе «Автоматизация отчетности, Excel, дашборды, API — БЕСПЛАТНО». Согласен, коммерчески не самое выгодное занятие, но что не сделаешь ради опыта :)

За месяц написали три человека. Разброс обращений удивил! Думаю, стоило конкретнее описать в объявлении перечень предоставляемых услуг. По итогу вот с чем ко мне обратились:

  1. Добавить музыку в презентацию

  2. Решить в Excel уравнивание одиночного полигонометрического хода любой формы двухгрупповым способом (без понятия, о чем идет речь)

  3. Сделать дашборды из СРМ «Мой Класс»

Я выбрал третий вариант. Задача так и звучала: «Сделать дашборды из СРМ Мой Класс». «Делов на пару вечеров», подумал я и написал «Да». Никакого дополнительного описания и требований не было. Меня это абсолютно никак не насторожило. Сейчас, оглядываясь назад, возникает ощущение: возможно, решить уравнивание одиночного полигонометрического хода было бы проще.

Пара слов про Авито

Как будто Авито не предназначено для простых смертных фрилансеров. За целый месяц на первое объявление откликнулось 3 человека. После задания из этой статьи я делал еще несколько объявлений, но уже ставил цену. И у меня не то что заказов не было, а даже просмотров. «Хочешь просмотры — плати, хочешь заказы — плати много!» — такое ощущение создалось у меня при работе с Авито. Даже опубликовать больше одного объявления бесплатно нельзя… Вопрос к экспертам: где искать заказы? :)

Без внятного ТЗ — результат ХЗ

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

Следующим сообщением после «Да» написал «Отправьте, пожалуйста, ТЗ. Что я должен сделать?». 

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

На самом деле требования продолжали уточняться на протяжении всего проекта, но если бы первым сообщением вместо «Нам нужно сделать дашборды из СРМ Мой Класс» отправили такое, то задача упростилась бы в разы:

После того, как разобрался с требованиями, начал думать над решением и инструментами, которыми буду пользоваться. На тот момент я только слышал о Google Apps Script, но не работал с ним.

Для начала работы мне требовался ключ API от их СРМ. Скорость и простота, с которой его дали, удивила. Представьте ситуацию: какой-то непонятный чувак (но с крутой аватаркой в ТГ), бесплатно решает их задачу, без какого-либо подтвержденного опыта и договора, просит дать полный доступ к их системе со всей конфиденциальной информаций. 

С одной стороны, по-другому их задачу не решить, с другой — им как будто стоило выразить хоть какие-то опасения на этот счет. Есть подозрение, что они не знали, на что способен человек с ключом API. Не подумайте, у меня не было желания им навредить или сделать что-то в этом духе. Просто, будь я на их месте, то не вписался бы в подобную историю, как-никак вопрос рисков :)

Дальше начинается самое интересное. Я быстро разобрался с документацией API, научился получать сведенья из CRM, но вот метрики, которые им были нужны, я всё никак не мог научиться считать. Чтобы я ни делал, показатели в моей таблице расходились с показателями, что были в их таблице, доступ к которой мне предоставили. Причина оказалась проста. У них в таблице были неправильные данные!

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

Это было последней каплей, я написал им, что намерен бросить задачу. Посудите сами:

  1. Раза три спрашивал: «Данные в таблице точно правильные?» Мне говорили: «Да»

  2. Уже почти месяц каждую неделю я тратил по несколько часов на неоплачиваемую и даже не на перспективную задачу

  3. В тот момент уже нашел работу и проходил адаптацию

Вы будете смеяться, но по итогу конфликт быстро исчерпал себя — мне предложили денег :)

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

После созвона дела пошли в гору, но не без сложностей. У этой CRM очень стрёмное и кривое API, приходилось делать много костылей, чтобы получить те или иные данные. В какой-то момент пришлось поменять архитектуру решения из-за ограничений Google, но об этом дальше.

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

Что находится под капотом решения?

Текущую ситуацию, еще до обращения ко мне, на стороне заказчика можно увидеть на следующей картинке:

Единственный вариант, который мне пришел в голову, для автоматизации этого процесса — использовать Google Apps Script. Он бесплатный, понятный и его легко передать заказчику в сопровождение. Остановимся на нем немного подробнее.

Google Apps Script — это облачный язык сценариев (основан на JavaScript), созданный Google для автоматизации и расширения возможностей своих сервисов (Google Таблицы, Документы, Почта, Календарь, Диск, Forms и т. д.). Это как «макросы в Excel» или VBA, но для Google, с бесплатным хостингом и доступом к API от Google. Ещё у этого инструмента есть возможность настраивать сценарии, т.е. можно задать когда и какую функцию нужно выполнить.

Как будто альтернатив нет (Или есть? Как бы вы решали задачу, если бы нельзя было использовать Apps Script? Напишите в комментарии, если есть идеи).

Немного подумав, наметил такое решение и приступил к реализации:

Я хотел как можно быстрее завершить эту задачу, поэтому начал решать в лоб. Логика простая — получить все данные, посчитать метрики и вывести их в таблицу. Мои романтические ожидания сразу разбились об ограничения Google, выставляемые к скриптам: время выполнения не должно превышать 6 минут. Из-за того, что программа делала все сразу, то время выполнения пробивало потолок в 6 минут.

Так появилась потребность в базе данных, чтобы сначала данные сохранить, а потом использовать их для расчета метрик. Напомню, решение нужно было бесплатное, поэтому базой данных стала отдельная вкладка в Google таблице :) 

На каждый тип данных (записи на занятия, абонементы, платежи и т. д.) сделал отдельную таблицу. На этом шаге я опять уперся в ограничения Google. Как я уже сказал, хотелось поскорее закончить эту задачу, поэтому решил особо не запариваться и при выполнении скрипта выгружать сразу все данные из CRM. Google подавился… Некоторых данных было так много, что скрипт не мог завершить выгрузку за 6 минут. Пришлось переделывать. 

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

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

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

P.s. Пока рисовал картинку, нашел ошибку, некритичную, но если ее устранить, то работать программа будет в разы быстрее. Я зачем-то считываю все данные и полностью перерисовываю отчет. Для десятков тысяч строк в таблицах это еще ок, но для сотен тысяч… Google снова подавится.

Чуть подробнее про функции

Кратко пробегусь по картинке. Сразу скажу, что почти весь код, который ниже привожу, я сократил, чтобы его было легче читать. Скрыл все костыли и оставил только смысловые элементы. Все довольно просто воссоздается при помощи нейронки, если это потребуется.

Функции скриптов получения данных:

  1. ф_авторизации() — нужна, чтобы СРМ могла вас идентифицировать, впустить и дать данные, когда вы попросите.

    function getToken() {
      let url = 'https://api.moyklass.com/v1/company/auth/getToken';
      let payload = { apiKey: 'ваш key' };
    
      let r = UrlFetchApp.fetch(url, {
        method:'post',
        contentType:'application/json',
        payload:JSON.stringify(payload),
        muteHttpExceptions:true
      });
    
      let a = JSON.parse(r.getContentText());
      return a.accessToken;
    }
  2. ф_поиска даты() — это, по сути, функция поиска даты срезки. Данные “после” этой даты обновятся, а данные “до” этой даты останутся неизменными. Реализация немного сложнее, но ее логика такая:

    function getCursor(n_month) {
      let today = new Date();
      today.setHours(0,0,0,0);
      
      let cutoffDate = today - n_month;
      return cutoffDate;
    }
  3. ф_получить данные() — нужна для того, чтобы отправлять запросы в CRM и получать от нее данные. Подобных функций несколько, они очень сильно похожи, отличаются по большей части архитектурой самого API. Для примера, эта функция получает платежи из CRM, есть еще три подобных. Одна для абонементов, другая для записей на занятия, другая для записей в группу. Не спрашивайте зачем им столько записей :)

    function getPayments(token, day1, day2) {
      let allData = []; let limit = 500; let offset = 0;
    
      while (true) {
        let url = 'https://api.moyklass.com/v1/company/payments?limit='+limit+'&offset='+offset+'&date='+day1+'&date='+day2; 
    
        let r = UrlFetchApp.fetch(url, {
          method:'get',
          headers:{'x-access-token':token,'Content-Type':'application/json'},
          muteHttpExceptions:true
        });
    
        // Отсюда убрал блок кода для большей читаемости
    
        let chunk = JSON.parse(r.getContentText()).payments;
        allData = allData.concat(chunk);
        offset += limit;
      }
      return allData || [];
    }
  4. ф_заполнить БД — выводит на нужную страницу данные, полученные шагом ранее.

    function printData(data) {
      // Отсюда убрал блок кода
    
      let header = Object.keys(data[0]);
    
      let rows = data.map(function(inv) {
        return header.map(function(key) {
          return inv[key] ?? "";
        });
      });
    
      sheet.getRange(3, 1, 1, header.length).setValues([header]);
      sheet.getRange(4, 1, rows.length, header.length).setValues(rows);
    }

Функции формирования отчетов:

  1. ф_очистить отчет — это даже не отдельная функция, а одна важная строчка —

     sheet.clear();
  2. ф_прочитать данные() — функция считывает данные со страницы. По идее, можно было обойтись командой:

    sheet.getDataRange().getValues(); 

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

  3. Дальше в цикле за определенный период считаю метрики и сохраняю их в массив. У меня получилось 7 разных метрик. Вот пример расчета одной из них:

    // Расчет прибыли за период. 
    function calcComing(payments, filIds) {
      let sumVal = 0;
      payments.forEach(p => {
        if (p.optype === 'income' && filIds.includes(p.filialId)) {
          if (typeof p.summa === 'number') sumVal += p.summa;
        }
      });
    
      return Math.round(sumVal * 100) / 100; // Округление до 2 знаков
    }
  4. ф_нарисовать отчет(): по факту, тоже одна строчка

    sheet.getRange(5,1,rows.length,header.length).setValues(rows);

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

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

Внутрянка бота - «рассыльщика» сообщений

Последовательность действий была такой:

  1. создать бота; 

  2. научить общаться бота и мою табличку;

  3. создать общий чат;

  4. добавить туда бота и людей. 

Пункт 1 делается через ТГ-бота BotFather — родителя всех ботов, так сказать. 

Пункт 2 состоит из двух шагов: сформировать и отправить текст. Про формирование текста писать ничего не буду — использовал имеющиеся функции. Отправка текста делается так:

function sendToTelegram(text) {
  let TOKEN = 'TOKEN';
  let CHAT_ID = 'CHAT_ID';

  let url = 'https://api.telegram.org/bot' + TOKEN + '/sendMessage';

  let payload = {
    chat_id: CHAT_ID,
    text: text,
    parse_mode: 'HTML'
  };

  UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });
}

Говоря про цифры. За всю работу я получил 10 000 руб. (Много или мало? Как думаете?). В итоговой версии программы около 1000 строк кода, на всю реализацию ушло 2.5 месяца. В неделю уходило часа по 4, может немного меньше. Сумму в час не хочу считать, не буду расстраиваться :)

Продолжение истории. Что было дальше?

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

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

Когда эта часть была настроена, мне пришла следующая мысль: «А если я захочу масштабировать это решение, то есть добавить новых пользователей, я смогу это сделать?». Я не хотел арендовать или покупать сервер, мне просто было интересно провести эксперимент, сделать своего рода MVP. И… Apps Script позволяет это сделать.

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

Чуть подробнее про техническую реализацию
  1. Для этого бота я использую вкладки Гугл таблицы для хранения упражнений и информации о пользователях, требуемой для рассылки. Есть функции для считывания и изменения этой информации. Например, считывания упражнения или изменения статуса пользователя - активен/ не активен.

  2. Функция отправки сообщения почти один в один такая же, как у бота из раздела 3. Называется она sendToTelegram().

  3. Что касается WebHook, то делается он очень просто. Нужно открыть в браузере страничку, которая формируется по определенным правилам (см. ниже).

    // При помощи этой ссылки создается WebHook
    https://api.telegram.org/bot<ТОКЕН_БОТА>/setWebhook?url=<ТВОЙ_ВНЕШНИЙ_HTTPS_АДРЕС>
    
    /* 1. <ТОКЕН_БОТА> - уникальный ключ, который присвается боту. Найти его 
    можно через BotFather в ТГ.
    2. <ТВОЙ_ВНЕШНИЙ_HTTPS_АДРЕС> - чтобы его получить нужно развернуть ваш 
    скрипт в Apps script как веб приложение, ссылка должна заканчиваться 
    на /exec */
    
    /* Дополнительно */
    //Открыв эту страницу в браузере можно узнать информацию о своем WebHook 
    https://api.telegram.org/bot<TOKEN>/getWebhookInfo
    
    //При помощи этой команды этот WebHook можно удалить
    https://api.telegram.org/bot<TOKEN>/deleteWebhook?drop_pending_updates=True
    

    Перед тем как поднимать свой WebHook, необходимо развернуть скрипт при помощи кнопки «Начать развертывание».

  4. Сердцем приема запросов является функция doPost(e). Она зарезервированная в Google Apps Script. Если вы захотите, что бы пользователь как-то взаимодействовал с ботом, то вся логика взаимодействия будет написана в этой функции.

    function doPost(e) {
      let data = JSON.parse(e.postData.contents);
      
      let msg = data.message;
      let user = msg.from;
      let chat = msg.chat;
    
      if (msg.text == "/start") {
        let status = "active"; // просто как пример
        sendMessage(chat.id, "Привет!");
      }
    
      return returnPlainText("ok");
    }
    
    function returnPlainText(text) {
      return HtmlService.createHtmlOutput(text)
        .setMimeType(HtmlService.MimeType.TEXT);
    }
    
    /* returnPlainText() нужна, чтобы текст «ok» apps script отправил в формате 
    текста, а не html. Если ТГ не получит «ok» текстом, то он войдет в 
    бесконечный цикл и начнет отправлять запросы в doPost, пока не получит 
    ответ «ok». */

Сейчас бот находится на стадии очень сырого MVP, но он работает. Если интересно, то могу дать ссылку для теста, для этого отправьте заявку. Ссылку в открытом виде пока не хочу оставлять. Как вы помните, у Google есть ограничения на скрипты, а значит, бота будет очень легко положить.

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

Бойтесь своих желаний

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

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

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

Есть у меня еще одно убеждение: главное — начать идти, а дальше на пути всё как-нибудь сложится. Найдутся нужные люди и появятся возможности. Для примера, недавно я выпустил статью (рекомендую к прочтению), и через какое-то время на меня вышла девушка с предложением: «Давай вместе сделаем статью. Я учусь на курсе по редактуре. Ты напишешь статью, а я помогу сделать ее круче!». Естественно я согласился. Кстати, это и есть наша совместная статья. Вот ее контакт, возможно, вы тоже напишете совместную статью!

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