При переходе на новые форматы фискальных данных и современные драйверы торгового оборудования (ДТО 10 от АТОЛ) разработчики часто сталкиваются с необходимостью интеграции старых платформ, таких как 1С 7.7, с веб-службами ККТ. В этой статье мы подробно разберем, как правильно организовать архитектуру запросов, какой COM-объект лучше использовать для стабильной связи, как обрабатывать JSON-ответы без лишней HTML-разметки и как решить проблему, когда сервер возвращает успешный код, но ККТ физически не печатает чек. Для реализации такого взаимодействия можно использовать готовое решение для работы 1С 7.7 с ККТ АТОЛ ДТО 10 через JSON-задания.
Встроенный Web-сервер АТОЛ (а в современных релизах — служба Web Requests) по умолчанию спроектирован как асинхронный сервис. Это критически важное архитектурное отличие от прямой работы через традиционный драйвер.
Когда вы отправляете POST-запрос с заданием (например, на открытие смены, закрытие смены или пробитие чека), веб-сервер выполняет лишь базовую валидацию структуры JSON-пакета. Если структура корректна, он моментально записывает задачу во внутреннюю очередь (базу данных SQLite) и возвращает HTTP-статус 201 Created вместе с уникальным идентификатором задания — uuid.
Важно понимать: получение статуса 201 означает лишь то, что задача успешно принята в очередь на сервере. Это не гарантирует, что принтер чеков в этот момент вообще включен или в нем есть бумага. Для получения реального результата работы ККТ необходимо реализовать двухэтапный алгоритм:
201 и извлекаем из тела ответа сгенерированный uuid.GET /api/v2/requests/<uuid>, пока статус задания во внутренней базе АТОЛ не изменится.Задание в очереди может принимать следующие статусы, которые возвращаются в JSON-ответе:
wait — задача ожидает своей очереди на выполнение;in_progress — задача передана драйверу и выполняется на ККТ;ready — задача успешно выполнена (только на этом этапе операция завершена);error — при выполнении произошла ошибка (в этом же ответе будет детальный код ошибки ККТ).Для работы с HTTP-запросами в среде 1С 7.7 разработчики часто выбирают между объектами Microsoft.XMLHTTP (или его версиями MSXML2.XMLHTTP) и WinHttp.WinHttpRequest.5.1. В качестве альтернативы штатным COM-объектам можно использовать внешнюю компоненту HTTP-клиента для 1С 7.7.
Рекомендуется использовать именно WinHttp.WinHttpRequest.5.1 по следующим причинам:
XMLHTTP изначально оптимизированы для работы со структурами XML и часто кэшируют GET-запросы, что критично при циклическом опросе статуса задания ККТ.XMLHTTP может вызывать утечки памяти при интенсивном сетевом обмене.WinHttp реализована более гибкая система настройки тайм-аутов соединения и ожидания ответа.Частой проблемой при первых запросах к веб-серверу АТОЛ является получение HTML-кода вместо структурированного JSON. Обычно это происходит по двум причинам: сервер вернул ошибку веб-сервера (например, 404 или 500) или в запросе не были явно указаны ожидаемые форматы данных.
Для того чтобы сервер возвращал только JSON, необходимо при инициализации запроса передавать правильные заголовки (Headers). Разберем по шагам, как настроить объект WinHttp в 1С 7.7:
// Создаем объект для работы с HTTP
WinHttp = СоздатьОбъект("WinHttp.WinHttpRequest.5.1");
// Инициализируем POST-запрос (например, на локальный сервер АТОЛ)
АдресСервера = "http://localhost:16732/api/v2/requests";
WinHttp.Open("POST", АдресСервера, 0); // 0 - синхронный режим ожидания отправки
// Указываем заголовки, сообщающие серверу, что мы передаем и хотим получить JSON
WinHttp.SetRequestHeader("Content-Type", "application/json; charset=utf-8");
WinHttp.SetRequestHeader("Accept", "application/json");
Использование свойства WinHttp.Status позволяет сразу получить числовой HTTP-код ответа (например, 201, 400, 409), минуя парсинг текста. Само тело JSON-ответа будет находиться в свойстве WinHttp.ResponseText.
В платформе 1С 7.7 отсутствуют встроенные средства для работы с форматом JSON (такие как ЧтениеJSON или ЗаписьJSON в 1С 8). Писать полноценный парсер через внешние библиотеки или регулярные выражения бывает сложно, поэтому рассмотрим эффективную рекурсивную процедуру разбора строки JSON в объект СписокЗначений.
Ниже приведен работающий алгоритм, который преобразует строку JSON в дерево списков значений, имитируя структуры и массивы:
//======================================================================
Процедура ЗаполнитьДанныеИзОтветаJSON(Результат, ТекстJSON, ТипДанных)
ТекстJSON = СокрЛП(Сред(ТекстJSON, 2));
НомерЗначения = 0;
Пока ТекстJSON <> "" Цикл
ПервыйСимвол = Лев(ТекстJSON, 1);
Если ПервыйСимвол = "{" Тогда
Значение = СоздатьОбъект("СписокЗначений");
ЗаполнитьДанныеИзОтветаJSON(Значение, ТекстJSON, "Структура");
Если ТипДанных = "Структура" Тогда
Результат.Установить("Значение" + ?(НомерЗначения = 0, "", НомерЗначения), Значение);
НомерЗначения = НомерЗначения + 1;
ИначеЕсли ТипДанных = "Массив" Тогда
Результат.ДобавитьЗначение(Значение);
КонецЕсли;
ИначеЕсли ПервыйСимвол = "[" Тогда
Значение = СоздатьОбъект("СписокЗначений");
ЗаполнитьДанныеИзОтветаJSON(Значение, ТекстJSON, "Массив");
Если ТипДанных = "Структура" Тогда
Результат.Вставить("Значение" + ?(НомерЗначения = 0, "", НомерЗначения), Значение);
НомерЗначения = НомерЗначения + 1;
Иначе
Результат.Добавить(Значение);
КонецЕсли;
ИначеЕсли (ПервыйСимвол = "}") И (ТипДанных = "Структура") Тогда
ТекстJSON = СокрЛП(Сред(ТекстJSON, 2));
Если Лев(ТекстJSON, 1) = "," Тогда
ТекстJSON = СокрЛП(Сред(ТекстJSON, 2));
КонецЕсли;
Возврат;
ИначеЕсли (ПервыйСимвол = "]") И (ТипДанных = "Массив") Тогда
ТекстJSON = СокрЛП(Сред(ТекстJSON, 2));
Если Лев(ТекстJSON, 1) = "," Тогда
ТекстJSON = СокрЛП(Сред(ТекстJSON, 2));
КонецЕсли;
Возврат;
Иначе
Если ТипДанных = "Структура" Тогда
Поз = Найти(ТекстJSON, ":");
Если Поз = 0 Тогда
Прервать;
КонецЕсли;
ИмяЗначения = СокрЛП(Лев(ТекстJSON, Поз-1));
ТекстJSON = СокрЛП(Сред(ТекстJSON, Поз+1));
Если Лев(ТекстJSON, 1) = "{" Тогда
Значение = СоздатьОбъект("СписокЗначений");
ЗаполнитьДанныеИзОтветаJSON(Значение, ТекстJSON, "Структура");
ИначеЕсли Лев(ТекстJSON, 1) = "[" Тогда
Значение = СоздатьОбъект("СписокЗначений");
ЗаполнитьДанныеИзОтветаJSON(Значение, ТекстJSON, "Массив");
Иначе
Поз = 0;
Для Сч = 1 По СтрДлина(ТекстJSON) Цикл
Символ = Сред(ТекстJSON, Сч, 1);
Если (Символ = ",") ИЛИ (Символ = "]") ИЛИ (Символ = "}") Тогда
Поз = Сч;
Прервать;
КонецЕсли;
КонецЦикла;
Если Поз = 0 Тогда
Значение = ТекстJSON;
ТекстJSON = "";
Иначе
Значение = Лев(ТекстJSON, Поз-1);
ТекстJSON = СокрЛП(Сред(ТекстJSON, Поз + ?(Сред(ТекстJSON, Поз, 1) = ",", 1, 0)));
КонецЕсли;
Значение = СокрЛП(Значение);
КонецЕсли;
Результат.Установить(ИмяЗначения, Значение);
ИначеЕсли ТипДанных = "Массив" Тогда
Поз = 0;
Для Сч = 1 По СтрДлина(ТекстJSON) Цикл
Символ = Сред(ТекстJSON, Сч, 1);
Если (Символ = ",") ИЛИ (Символ = "]") ИЛИ (Символ = "}") Тогда
Поз = Сч;
Прервать;
КонецЕсли;
КонецЦикла;
Если Поз = 0 Тогда
Значение = ТекстJSON;
ТекстJSON = "";
Иначе
Значение = Лев(ТекстJSON, Поз-1);
ТекстJSON = СокрЛП(Сред(ТекстJSON, Поз + ?(Сред(ТекстJSON, Поз, 1) = ",", 1, 0)));
КонецЕсли;
Значение = СокрЛП(Значение);
Результат.Добавить(Значение);
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
//======================================================================
Функция ЗаполнитьСтруктуруИзОтветаJSON(Знач ТекстJSON)
Результат = СоздатьОбъект("СписокЗначений");
// Очищаем экранирование кавычек
ТекстJSON = СтрЗаменить(ТекстJSON, "\""", """");
ТекстJSON = СтрЗаменить(ТекстJSON, """", "");
Если Лев(ТекстJSON, 1) = "{" Тогда
ЗаполнитьДанныеИзОтветаJSON(Результат, ТекстJSON, "Структура");
ИначеЕсли Лев(ТекстJSON, 1) = "[" Тогда
МассивДанных = СоздатьОбъект("СписокЗначений");
ЗаполнитьДанныеИзОтветаJSON(МассивДанных, ТекстJSON, "Массив");
Результат.Вставить("Значение", МассивДанных);
КонецЕсли;
Возврат Результат;
КонецФункции
Рассмотрим практическую ситуацию: мы отправили JSON-запрос открытия смены, получили статус 201, но касса не открыла смену и ничего не напечатала. Выясним причину.
Для этого мы должны использовать полученный при создании задачи uuid. Алгоритм действий выглядит следующим образом:
uuid из ответа первого запроса. Например, из JSON-строки {"uuid": "9c8a1b2c-3d4e-5f6a-7b8c-9d0e1f2a3b4c", "status": "wait"} с помощью нашего парсера получаем значение ключа uuid.http://localhost:16732/api/v2/requests/9c8a1b2c-3d4e-5f6a-7b8c-9d0e1f2a3b4c.error, внутри структуры ответа будет подробный блок ошибок драйвера и ККТ. Например, мы сможем узнать, что ККТ заблокирована, закончилась чековая лента, открыта крышка принтера или смена превысила 24 часа.Пример организации цикла ожидания выполнения в 1С 7.7:
// Инициализируем цикл опроса статуса
МаксПопыток = 10;
ПопыткаОбработки = 1;
УспешноВыполнено = 0;
Пока ПопыткаОбработки <= МаксПопыток Цикл
// Формируем GET-запрос к статусу задачи
АдресСтатуса = "http://localhost:16732/api/v2/requests/" + ВыделенныйUUID;
WinHttp.Open("GET", АдресСтатуса, 0);
WinHttp.SetRequestHeader("Accept", "application/json");
WinHttp.Send();
Если WinHttp.Status = 200 Тогда
СтруктураОтвета = ЗаполнитьСтруктуруИзОтветаJSON(WinHttp.ResponseText);
ТекущийСтатус = "";
СтруктураОтвета.Получить("status", ТекущийСтатус);
Если ТекущийСтатус = "ready" Тогда
УспешноВыполнено = 1;
Прервать;
ИначеЕсли ТекущийСтатус = "error" Тогда
// Извлекаем ошибку
КодОшибки = "";
ОписаниеОшибки = "";
// Обработка вложенной структуры ошибок ККТ
Предупреждение("Ошибка выполнения на ККТ! Описание: " + ОписаниеОшибки);
Прервать;
КонецЕсли;
КонецЕсли;
// Небольшая пауза перед повторным запросом
// В 1С 7.7 паузу можно сделать через вызов пинга локального хоста или WScript.Shell
СекундПаузы = 1;
Shell = СоздатьОбъект("Wscript.Shell");
Shell.Run("ping 127.0.0.1 -n " + СокрЛП(СекундПаузы + 1), 0, 1);
ПопыткаОбработки = ПопыткаОбработки + 1;
КонецЦикла;
Если УспешноВыполнено = 1 Тогда
Сообщить("Операция на ККТ успешно завершена.");
КонецЕсли;
Если вы планируете использовать современные релизы ДТО (например, 10.9.x или 10.10.x), вам необходимо учесть несколько важных изменений в архитектуре АТОЛ:
atol-fptr-web-requests-users.exe add <логин> <пароль>
Authorization в формате Basic Base64.[96] Отсутствует лицензия на работу с Web-сервером. Для исправления ситуации необходимо обратиться в авторизованный сервисный центр АТОЛ для генерации кода лицензии.