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


Честно, нас удивила простота входа в этот облачный мир, его масштабы (160 тысяч пользователей практически на старте), возможности и… дыры. В итоге у нас всё получилось, а компания-организатор, по их заявлениям, вот-вот стартует проект маркетплейса.


В этой статье я покажу как за 2-3 часа был сделан веб-сервис, интегрированный с облачными кассами и пригодный для обкатки на фокусной группе клиентов. Сам проект доступен для ознакомления и клонирования в виде исходников в github и онлайн версии.




На хакатон мы пришли не то что без наработок, а вообще смутно представляя, о чем речь. Единственный человек, который был в курсе и который нас подписал сюда, разработкой не занимается. Поэтому нужно было всё сделать очень быстро — ТЗ писали сразу в платформе, отлаживая его работу.


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


Цель хакатона была — создать приложение, которое будет доступно в магазине облачного сервиса и будет выполнять какие-то полезные клиентуре функции. Основная задача нашего приложения — зарегистрировать пользователя в сервисе и дать ему быстрый доступ в личный кабинет Маркетплейса. В мобильном приложении Маркетплейс тоже работает в виде web-view, но отлаживать сервис под разные мобильные платформы мы, разумеется, не стали.


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


Схема его данных в Редакторе типов платформы выглядит как на картинке ниже — так начинается наше ТЗ.


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



Впрочем, схема данных скрыта от пользователей системы, и только администратор может изменять её.


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




Мы повторили структуру данных облака, и в меню Синхронизация данные в сервисе обновляются: создаются магазины, товары и прочие объекты, обновляются цены и т.д. Страница синхронизации, приведенная на рисунке выше, отрисовывает javascript’ом все объекты, полученные в JSON из облака и из базы сервиса.


Сам код страницы синхронизации, если кому интересно, доступен в гитхабе. Он сделан на голом javascript, там 7 килобайт кода, которые, наверняка, можно сократить, но для хакатона это было не важно.


Много букв, как всё устроено внутри

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


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


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



Выбранные пользователем поля отчета находятся в подчиненных таблицах Колонки отчета, которые подробно рассмотрены в скрытом тексте ниже по статье.


Служебные отчеты мы назвали с префиксом int. Пользовательские отчеты именуются по-русски, а доступ пользователей к ним ограничен по маске имени отчета, указанной в Роли (все детали также описаны в скрытом тексте, если вдруг кому будет интересно).


Про служебные отчеты

Отчеты intMyShop и intMySupp возвращают соответственно код Магазина и Поставщика, связанных с пользователем. Например, intMyShop внутри выглядит так:



Выполняя запрос для этого отчета, движок платформы выберет из базы все ссылки на Пользователя, которые равны ID пользователя, запустившего отчет ([USER_ID] и [USER] — ID и имя пользователя, параметры среды исполнения). Также будут выбраны соответствующие Магазины. Применяемая здесь функция abn_ID возвращает ID объекта, а не его имя, потому что в качестве ссылки используются идентификаторы. Кроме того, сравнивать числовые ID быстрее, чем символьные имена объектов.


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



Таким образом, при создании заказа в него сразу будут подставлены текущая дата, код статуса «Новый» (737) и код Магазина, к которому привязан пользователь.


Стоять! Что за код статуса «Новый»?

В Словаре сервиса мы можем посмотреть справочник Статусов, где можно узнать коды (системные ID), которые подставляются в заказ для указания на его статус.




Отчет intProds собирает информацию о товарах Магазина, к которому привязан пользователь. Здесь перечислены колонки отчета, которым заданы имена, используемые затем в шаблоне:



Возвращаемый отчетом набор записей выглядит примерно так:



Этот отчет используется в форме синхронизации для заполнения массива javascript в таком фрагменте кода:


<script>
var s=new Array(),
     p=new Array();
<!-- Begin:intShops -->s['{uid}'] = {i:{id}, n:'{n}'};
<!-- End:intShops -->
<!-- Begin:intProds -->p['{uid}'] = {i:{id},
 s:'{sid}',
 n:'{tname}',
 stock:'0{stock}',min:'0{min}',ask:'0{ask}',costPrice:'{price}'};
<!-- End:intProds -->
</script>

Конструкция <!-- Begin:intProds -->...<!-- End:intProds --> велит парсеру вызвать отчет intProds, а колонки его результата использовать для заполнения точек вставки {uid}, {id}, {sid} и других внутри этой конструкции. Фрагмент кода внутри точки вставки повторится столько раз, сколько записей вернет отчет.


Обработав этот код, парсер заполнит его данными, получив исполняемый javascript, что-то вроде этого:


<script>
var s=new Array(),
    p=new Array();
s['20171202-0534-4070-8033-06A9159C64BE'] = {i:529, n:'Мой магазин'};
s['20171204-C914-409F-8028-405CA94D6FE4'] = {i:1178, n:'Дубль магаз'};

p['70b263e5-252f-4a77-949e-22f4ed7dedab'] = {i:547,
   s:'20171202-0534-4070-8033-06A9159C64BE',
   n:'винегрет с селедкой 100/50 г.',
   stock:'05',min:'07',ask:'020',costPrice:'48'};
p['2e3a61d4-f465-4ec9-ad86-a84e53b4402a'] = {i:549,
   s:'20171202-0534-4070-8033-06A9159C64BE',
   n:'Набор маркеров перманентных СС1950 3-5мм 4цвета',
   stock:'050',min:'0103',ask:'0100',costPrice:'42'};
p['581c8f9b-6a19-42bc-9a36-169e792165bc'] = {i:551,
   s:'20171202-0534-4070-8033-06A9159C64BE',
   n:'платочки бумажные зева кидс',
   stock:'050',min:'040',ask:'01000',costPrice:'8'};
p['11e59bcf-ea73-4fbd-adb2-955fd3452749'] = {i:553,
   s:'20171202-0534-4070-8033-06A9159C64BE',
   n:'Кунжутный грильяж мелешин',
   stock:'015',min:'040',ask:'00',costPrice:'125'};
p['7a631129-05c0-4cf3-a9d7-dfbd56d31e68'] = {i:555,
   s:'20171202-0534-4070-8033-06A9159C64BE',
   n:'Draje',
   stock:'050',min:'0100',ask:'01000',costPrice:'44'};
</script>


Далее эти массивы используются для отрисовки таблиц синхронизации с облаком.



Коротко об организации доступов ролей

В сервисе есть три роли пользователя: Продавец (User), Поставщик и Логистическая компания. Вот так пользователи выглядят в Словаре:



Роли пользователя задано меню, отображаемое пользователю — набор пунктов в виде ссылок на отчеты.



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



Для роли указан набор объектов, маска для ограничения видимости и уровень доступа — запрет, чтение, запись.


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


Есть еще служебный пользователь Reg, который может создавать пользователей с ролью User (и только ей — доступ к справочнику ролей ограничен маской). Он не может просматривать пользователей, потому что маска «!%» задает «пустое» имя пользователя, чего быть не может. Этот пользователь используется формой регистрации на сайте, которая после проверки капчи и валидации полей может сделать запись о новом пользователе, отправив сервису POST-запрос, имитирующий отправку формы создания пользователя.


Исходник отчета по доступам крайне прост:




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


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

Продавец


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



Мы указываем нужные значения, и они сохраняются с привязкой к товару.


Показать как они сохраняются

По нажатию кнопки вызывается javascript, асинхронно сохраняющий данные GET-запросом


// Save the parameters for this Product
function saveDiv(d){
  save = new XMLHttpRequest(); 
  save.open('GET'
    ,'index.php?db={_global_.z}&a=edit_obj&next_act=nul&do=save_val&id='+p[d].i
      +'&t216='+document.getElementById('s'+d).value
      +'&t217='+document.getElementById('m'+d).value
      +'&t716='+document.getElementById('a'+d).value
	,true); 
  save.send();
  save.onload=function(e) {
     save.abort();
     // Light the "Saved Ok" icon
     document.getElementById('b'+d).style.display='inline';
  }
}

Этот запрос эмулирует отправку формы, как если бы пользователь нашел эту номенклатуру в Магазине в базовом интерфейсе, отредактировал и нажал сохранить:



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


Показать исходный код формы базового интерфейса
<FORM method="post" action="index.php?db=evo&a=edit_obj" ENCTYPE="multipart/form-data" ONSUBMIT="savebtn.disabled=true; return true;">
<TABLE >
  <TR>
    <TD>553    <B>Номенклатура (код):</B>
      <input type="hidden" name="do" value="save_val">
      <input type="hidden" name="typ" value="211">
      <input type="hidden" name="id" value="553">
    </TD>
  </TR>
  <TR>
    <TD><input type="text" name="t211" value="11e59bcf-ea73-4fbd-adb2-955fd3452749" autofocus  class="form-control">
      <div style="height:5px; "></div>
    </TD>
  </TR>
  <TR>
    <TD>
    <div style="height:3px; "></div>
    <TABLE class="table table-condensed">
    <TR>
      <TD style=" max-width:400px; " ALIGN="right">
        Товар: 
      </TD>
      <TD>
        <nobr><select name="t213"  class="form-control">
          <option> </option>
          <option value="1139">"модняшка"-штаны</option>
          <option value="532">Draje</option>
          <option value="536">винегрет с селедкой 100/50 г.</option>
          <option value="535" SELECTED>Кунжутный грильяж мелешин</option>
          <option value="980">Лейка д/душа SH619/ТАНГО</option>
          <option value="1170">Льняное масло 3 л</option>
          <option value="533">Набор маркеров перманентных СС1950 3-5мм 4цвета</option>
          <option value="539">Напиток энергетический б/а газ. "Ред Булл" 0,25л.</option>
          <option value="534">платочки бумажные зева кидс</option>
          <option value="537">САЛФЕТКИ ВЛАЖН. АУРА BEATY YOUNG ОСВЕЖАЮЩИЕ 15ШТ</option>
          <option value="538">Солодка корень 1,2 кг</option>
          </select>
        </nobr>
      </TD>
    </TR>
    <TR>
      <TD style=" max-width:400px; " ALIGN="right">
        Остаток: 
      </TD>
      <TD>
        <input class="form-control" type="text" name="t216" size="10" value="15" >
      </TD>
    </TR>
    <TR>
      <TD style=" max-width:400px; " ALIGN="right">
        Неснижаемый остаток: 
      </TD>
      <TD>
      <input class="form-control" type="text" name="t217" size="10" value="40" >
      </TD>
    </TR>
    <TR>
      <TD style=" max-width:400px; " ALIGN="right">
        Минимальный заказ: 
      </TD>
      <TD>
        <input class="form-control" type="text" name="t716" size="10" value="0" >
      </TD>
    </TR>
    <TR>
      <TD style=" max-width:400px; " ALIGN="right">
        Цена закупки: 
      </TD>
      <TD>
        <input class="form-control" type="text" name="t1025" size="10" value="125" >
      </TD>
    </TR>
    </TABLE>
    </TD>
  </TR>
  <TR>
    <TD colspan="2">
      <input  type="submit" name="savebtn" class="btn btn-primary" value="Сохранить">
      <input  type="submit" name="copybtn" class="btn btn-default" value="Копия">
    </TD>
  </TR>
</TABLE>
</FORM>

* Из кода формы удалена часть косметической и другой второстепенной информации



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


Стоит заметить, что в облаке применен очень удачный подход — использование единого справочника популярных названий товара для всех — это минимизирует конфликты названий. Пользователю «навязывается» наиболее употребляемый вариант.


Про номенклатурные коды

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



Товары существуют в независимом справочнике, а вот номенклатуры, в которых используются их названия, привязаны к конкретным магазинам вместе с сохраненными пользователем параметрами:




Поставщик


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



У товаров может быть несколько штрих-кодов, по которым можно их искать, хотя в таблице приведен только один из них.


Ищем штрих-код, заканчивающийся на %7777 (например, товар Ред Булл имеет несколько разных кодов), видим, как это работает:



Исходник этого отчета


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


Здесь мы также собираем html-код «кнопки» — см. поле Формула, которая будет нарисована в отчете и запустит действие создания объекта «Предложение» (тип 591), заполнит его реквизит «Товар» (тип 593).


Хакатон проводился до того, как мы внедрили ЧПУ в ядре платформы, поэтому тогда относительные адреса выглядели как


index.php?db=evo&id=591&a=edit_obj&do=new_obj&t593=prod

Теперь тот же адрес выглядит так:


evo/new_obj/591?t593=prod

что более приятно видеть человеку.



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


Предложение мы сохраняем в виде даты его создания:



У предложения есть системный ID=1238, значение 3.12.2017 и реквизиты, часть которых заполняется автоматически.


Список всех предложений Поставщика (меню Мои предложения) позволяет отбирать и редактировать предложения, удалять неактуальные. Для этого отчеты делаются интерактивными, чтобы можно было быстро перейти к объектам из отчета, в данном случае кликнув значение «Дата»:



Исходник отчета выглядит так


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


  1. Значение контекстного параметра [USER_ID] — идентификатор авторизованного пользователя — сравнивается c идентификаторами (ID) Пользователей системы
  2. У найденного по ID Пользователя есть ссылка на Поставщика, по которой последний будет найден в базе
  3. Далее будут выбраны все Предложения данного Поставщика, которые текущий пользователь увидит в отчете


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


Почему это работает именно так?


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


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



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



Исходник отчета изнутри


Этот запрос к базе выведет предложения Поставщиков, отсортированные по актуальности: в 8-й колонке считается коэффициент заполнения минимального остатка, по возрастанию которого идет сортировка. Также отчет формирует гиперссылку для заказа товара в 10-й колонке. Кроме этого в отчете можно увидеть количество уже заказанного товара по данному предложению и последнюю цену закупки.



Здесь можно сразу сделать заказ, в два клика. Клики отмечены цифрами 1 и 2, все поля формы уже автоматически заполнены.



Поля Отправка и Доставка (это даты со временем) недоступны для редактирования, так как их будет заполнять логистическая компания.


Далее мы наблюдаем за состоянием своих заказов и предполагаемым временем их доставки.



Поставщик также видит относящиеся к нему заказы и планируемые даты отгрузки, может подготовиться к отгрузкам и планировать свои закупки.



Логистическая компания


Третий участник процесса — логистическая компания. Основное его рабочее место — это список заказов:



Исходник отчета


Отчет совсем простой — выбраны колонки из четырёх разных таблиц, к ним применен фильтр: статус заказа не дожен быть «Закрыт».



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


Логистика может быть достаточно сложна, но мы здесь пока видим только смену статусов заказов и даты отправки и доставки товара.



Для логистов на данном этапе нет фильтра по компании или пользователю — для обкатки будет использоваться единственная компания с двумя-тремя диспетчерами.


Выполненный заказ автоматически убирается из списка заказов Продавца, но доступен в истории заказов через фильтр по статусам заказов: % (все статусы).



На этом завершается полный цикл работы сервиса для пилотных клиентов.


Как видите, нам не понадобилось ничего, кроме платформы прототипирования и текстового редактора для создания этого проекта (стартапа!). Мы сэкономили достаточно много времени, используя свою ORM, в чем вы можете убедиться, если попробуете сделать то же самое в любой CRM, конструкторе или с нуля в вашей любимой платформе разработки. Все шаги разработки помещены выше в скрытый текст и, по сути, выполнены на ваших глазах. Мне кажется, всё это отражено достаточно доступно и не требует специальных знаний для работы со схемой данных и запросами.


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

Комментарии (3)


  1. Asmodeux
    21.05.2018 19:00

    Где-то здесь подвох…
    Выглядит вроде логично, придраться не к чему. Вас за это закидали красными?


  1. Alexey_mosc
    21.05.2018 19:43

    Выглядит интересно. По сути, имея облако для синхронизации, можно быстро запилисть магазин!


    1. UltimaSol Автор
      21.05.2018 20:20
      +1

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