Зачем я вообще взялся за всё это? В одном небольшом розничном магазине стоит POS-система с не самыми гибкими настройками. Нужно было сделать так, чтобы продавец не видел остатки товаров в магазине — он продаёт только то, что отображается. Но! В некоторых разделах каталога остатки всё же должны быть видны. Для чего — это уже вопрос бизнес-логики.
Конечно можно было бы использовать API POS-системы, но они нагло запросили за это денег😁
Логинюсь в POS-систему под админом и открываю dev-tools браузере. Вижу, что используется reactjs, что для меня было хорошо. Перехожу в каталог. В каталоге все товары разбиты по категориям, открываю категорию и вижу, что данные приходят по Ajax. Соответственно, если данные приходят через ajax, то на бэкенд должен отправляться какой-то ключ аутентификации (jwt, access-token и т.д.). Это в общем база при HTTP запросах. Остаётся понять, как это работает.
curl 'https://my-pos-system.ru/service/?x_version=25.2155-162.10' \
-H 'accept: application/json, text/javascript, */*; q=0.01' \
-H 'accept-language: ru-RU;q=0.8,en-US;q=0.5,en;q=0.3' \
-H 'cache-control: no-cache' \
-H 'content-type: application/json; charset=UTF-8' \
-H 'origin: https://my-pos-system.ru' \
-H 'pragma: no-cache' \
-H 'priority: u=1, i' \
-H 'referer: https://my-pos-system.ru/page/nomenclature-catalog' \
-H 'sec-ch-ua: "Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "macOS"' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: same-origin' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' \
-H 'x-calledmethod: Nomenclature.List' \
-H 'x-originalmethodname: Nf32Jkl4342nafi4=' \
-H 'x-requested-with: XMLHttpRequest' \
-b $'lang=ru; region=RU; DeviceId=qqqq-qqqq-qqqq-qqqq-qqqq; _ym_uid=123; _ym_d=1234; ... и далее много чего в куках' \
--data-raw '{"jsonrpc":"2.0","protocol":7,"method":"Nomenclature.List","params": ... и далее большой json в теле запроса'
Выполнил в консоли запрос и пришёл необходимый мне ответ от POS-системы. Это было небольшой успех, т.к. значит, можно отравлять и получать запросы.
Изучив curl запрос, предположил, что данные для аутентификации передаются через cookie. Удалил всё лишнее и выполнил запрос
curl 'https://my-pos-system.ru/service/?x_version=25.2155-162.10' \ -b $'lang=ru; region=RU; DeviceId=qqqq-qqqq-qqqq-qqqq-qqqq; _ym_uid=123; _ym_d=1234; ... и далее много чего в куках' \ --data-raw '{"jsonrpc":"2.0","protocol":7,"method":"Nomenclature.List","params": ... и далее большой json в теле запроса'
В ответ пришел 403 HTTP код. Значит удалил, что-то лишнее. Начал потихоньку пробовать возвращать параметры в curl и через пару минут понял, что не хватает заголовка -H 'content-type: application/json; charset=UTF-8'
Добавил в запрос и выполнил
curl 'https://my-pos-system.ru/service/?x_version=25.2155-162.10' \ -H 'content-type: application/json; charset=UTF-8' \ -b $'lang=ru; region=RU; DeviceId=qqqq-qqqq-qqqq-qqqq-qqqq; _ym_uid=123; _ym_d=1234; ... и далее много чего в куках' \ --data-raw '{"jsonrpc":"2.0","protocol":7,"method":"Nomenclature.List","params": ... и далее большой json в теле запроса'
Необходимый ответ пришёл. Это успех. Значит можно переходить к кодингу.
Ответ в JSON там очень большой, подробно разбирать его не будет, это не сильно интересно. Много полей с названиями полей f, a, t и т.д., но опытным путём нашёл какие поля необходимы. Мне нужно было найти только название номенклатуры, описание и остаток. Кто в своей юности использовал ArtMoney для получения бесконечных жизней и ресурсов в играх меня поймут. Взял первоначальный ответ, потом поправил в необходимые параметры в POS-системе и снова сделал запрос. Нашёл поля, которые изменились, они то мне и нужны.
Стек
К выбору стека исходил из своих навыков. Можно было конечно взять C++ и начал писать, но это не мой случай. Нужно быстро и чтобы я мог поддерживать это. Оценив свои навыки PHP, Golang, JS (TS), немного reactjs, начал гуглить, что вообще есть. Есть отличный фреймворк electron на котором написано ряд популярных приложений (slack, postman).
В общем взял electron начал создавать проект и компилировать его. Из плюсов, он очень мощный, можно копилить сразу под разные платформы. Развернул проект, начал компилить и у меня упорно не хотела происходить сборка. Потратив вечер на борьбу с electron, не хотел тратить много времени, решил погуглить ещё варианты. Нашёл фреймворк Wails на котором можно писать на Golang + JS (TS). Развернул и скомпилировал пустой проект за 5 минут. Было принято решение использовать его.
По итогу экспериментов на пустом проекте у меня получился такой стек:
Golang — backend-прослойка и основа приложения.
Wails — фреймворк для сборки desktop-приложений с UI на web-технологиях и backend на Go.
React — библиотека для построения интерфейсов.
TypeScript — типизированное надмножество JavaScript, упрощающее разработку и отладку.
MUI (Material UI) — готовый набор компонентов интерфейса в стиле Material Design.
Такой стек позволяет быстро разрабатывать современное desktop-приложение с мощной логикой на Go и удобным интерфейсом на React.
Инициализация wails
Первым шагом создадим базовый каркас desktop-приложения с использованием Wails
Установка Wails
Перед установкой убедитесь, что у вас установлен Go (версия 1.20+) и Node.js. Затем установим сам CLI:
Проверьте, что всё установилось корректно:
Если появилась надпись SUCCESS Your system is ready for Wails development! значит всё установилось и готово к работе
Создание проекта каркаса проекта
Создаём новый проект с шаблоном React + Vite + TypeScript:
wails init -n catalog-desktop -t react-ts
В результате структура проекта будет выглядеть примерно так:
Запуск в режиме разработки
Перейдём в директорию проекта и запустим в dev-режиме:
cd catalog-desktop
wails dev
Эта команда одновременно запускает frontend (с hot reload через Vite) и backend на Go. Любые изменения в интерфейсе или логике пересобираются автоматически.
При успешной сборке у вас откроется окно приложения
Создание UI
Для начала установим зависимости которые нам необходимы для UI.
Переходим в папку frontend и выполняем команду
Требования у меня были следующие:
- Отобразить данные в виде таблицы
- При запуске приложения сходить в POS-систему получить необходимые данные
- Необходимо обновлять данные из POS-системы раз в 1 минуту
- Отобразить кнопку обновить, чтобы кассир мог вручную обновить данные из POS-системы
Вот так я это примерно видел
Постараюсь описать в статье как можно меньше кода, т.к. в конце будет репозиторий со всем кодом.
Отобразить данные в виде таблицы
Для начала набросал UI, как это должно выглядеть.
Особо красивый код я не стал делать, поэтому пошло как пошло.
По итогу получился вот такой "дизайн"
При запуске приложения сходить в POS-систему получить необходимые данные
Забегая немного вперед, после написания кода для получения необходимых данных из POS-систему я столкнулся с CORS проблемой. Как я это решал, будет позже, пока пишем код.
Накидал метод handleRequest который будет отвечать за запрос к POS-системе. Он будет использоваться при старте приложения, при автоматическом обновлении и хэндлером для кнопки ручного обновления.
Так же решил добавить лоадер и вывод ошибок
Необходимо обновлять данные из POS-системы раз в 5 минут
Тут всё просто, особо говорить нечего. Запускаем таймер и дёргаем handleRequest
Отобразить кнопку обновить, чтобы кассир мог вручную обновить данные из POS-системы
Тут ещё проще, берём handleRequest и навешиваем на событие onClick единственной кнопки UI
Проблема с CORS
Как я уже упоминал, столкнулся с проблемой CORS — политики безопасности браузеров, которая блокирует запросы между разными доменами. Из-за этого нельзя напрямую отправлять запросы в POS-систему. Чтобы обойти это, сервер должен возвращать в заголовках значение Access-Control-Allow-Origin, разрешающее такие запросы.
Чтобы обойти эту проблему, нужен был прокси. Самый простой вариант — поднять веб-сервер прямо в приложении и ходить через него в POS-систему. Но у Wails есть одна интересная фишка: можно писать логику на Go и вызывать её из JavaScript. Магия, подумал я — и решил попробовать.
Немного вайбкодинга(да я вначале хотел проверить теорию, сработает ли и только потом углубляться в технические составляющие) и у меня получился вот такой комит.
Немного теории о Wails и IPC
Wails для этой магии использует IPC(Inter-Process Communication). IPC (Inter-Process Communication) — это набор механизмов, позволяющих двум или более процессам обмениваться данными между собой.
В случаем wails используется WebView messaging. WebView messaging — это механизм обмена сообщениями между веб-содержимым (WebView) и внешним приложением. Он позволяет вызывать действия на стороне приложения в ответ на события, происходящие в WebView, и наоборот. При этом запрос не отправляется через сеть.
Примерно так выглядит жизненный цикл запроса
[Frontend] window.backend.MyService.Hello("World")
↓ (WebView messaging)
[Nativе Bridge] Получаем JSON: {service: "MyService", method: "Hello", params: ["World"]}
↓
[Go] Выполняем метод MyService.Hello("World") → "Hello World"
↓
[Nativе Bridge] Возвращаем результат
↓
[Frontend] Promise resolved → "Hello World"
Сборка
Для сборки приложения достаточно выполнить команду wails build. В результате в папке build/bin будет создан исполняемый файл.
Главное преимущество Wails в том, что он не включает в себя целый браузер, как это делает Electron. Благодаря этому итоговый размер моего приложения составил всего 9.5 МБ.
Но есть нюанс — для каждой платформы (Windows, Linux, MacOS) придётся собирать приложение в её родной среде.
Итоги
После нескольких вечеров экспериментов с Electron и Wails мне удалось решить поставленную задачу. Код, конечно, пока далёк от идеала — многое можно переписать и оптимизировать. Кто-то может указать на небезопасность хранения авторизационной куки прямо в приложении, но в моём случае вопрос безопасности был не приоритетом: приложение должно работать в единственном экземпляре на рабочем месте кассира.
С развитием ИИ подход к таким задачам сильно упростился — просто набрасываешь код, запускаешь, проверяешь, как работает, и уже по ходу разбираешься, что и как. Быстро, гибко и эффективно — настоящий вайб кодинга в наше время.
👉 Подписывайтесь на по ТГ буду стараться писать что-то полезное и интересное https://t.me/+fhVmaCi66s9kMDBi