1. Выбор способа обмена. Описание API.

2. Реализация API на стороне 1С.


3. Приложение на ТСД и связь с 1С: Предприятие 8.3 через HTTP-Сервис. Часть 3(BroadcastReceiver. Получаем данные)

Пособий как создать HTTP-сервис в интернете предостаточно. Поэтому сразу опишу реализацию. Сервис у нас состоит из трех шаблонов URL:

  • Справочники — /v1/catalogs/{Справочник}/{Действие}
  • Документы — /v1/documents/{Документ}/{Действие}
  • Сервисы — /v1/services/{Действие}


Для того чтобы не раздувать статью, рассмотрим пример со справочником «Номенклатура». 1С нам не позволяет иметь разные пути для HTTP-методов По этому шаблон URL будет (/v1/catalogs/wares/{Действие}, для которого определены два HTTP метода GET, POST. В других шаблонах также будем использовать GET, POST.
  • /v1 — позволит нам в будущем поддерживать несколько вариантов API. Мы сможем добавить шаблон URL Справочники_v2 и соответственно путь начать с /v2
  • /catalogs/wares/catalogs объединяет все справочники. Это статичная часть пути. wares соответствует справочнику «Номенклатура». 1С поместит «wares» в ПараметрыURL. Тут надо отметить, что пути мы придумываем сами. И как их потом обрабатывать в 1С думаем тоже сами
  • {Действие} — сюда мы будем передавать что же мы хотим получить от 1С. На пример getByParam или getAll


С точки зрения Retrofit baseUrl будет выглядеть примерно так "http://192.168.0.1/unf/hs/inntsd/v1/" где:

  • 192.168.0.1 — Сервер где опубликован HTTP-сервис
  • /unf — имя базы данных 1С
  • /hs — статическая часть для HTTP-сервисов
  • /inntsd — корневой URL для всего сервиса. Обзываем как хотим


Рассмотрим процедуру получения запроса.

Функция СправочникиGet(Запрос)
//Разбираем параметры запроса. Заполнение структуры выполняется одинаково и для Get и для //Post  
	СтруктураЗапросаСправочник = ВернутьПараметрыЗапросаСправочник(Запрос, "GET");
	
//middleware. Проверим что пришли все необходимые данные. 
	РезультатПроверки = ПроверитьСтруктуруЗапроса(СтруктураЗапросаСправочник);
	
	Если РезультатПроверки Тогда
		СтруктураОтвета	= ОбработатьЗапросСправочник(СтруктураЗапросаСправочник);

Функция ВернутьПараметрыЗапросаСправочник(Запрос, МетодЗапроса) Экспорт
	
	Справочник		= ПолучитьИмяСправочника(Запрос); // получим переменную {Справочник}
	Действие			= ПолучитьДействиеСправочник(Запрос); // получим переменную {Действие}
	ПараметрыСтроки	= ПолучитьПараметрыИзСтроки(Запрос);  // получим список  из Параметров URL. В функции одна строка Возврат Запрос.ПараметрыURL
	ПараметрыТела	=ПолучитьПараметрыИзТела(Запрос); // Советую взглянуть на эту функцию поближе. Поможет понять почему в Firefox работает, а Google Chrome нет
	
	СтруктураЗапроса = Новый Структура;
	СтруктураЗапроса.Вставить("МетодЗапроса", МетодЗапроса);
	СтруктураЗапроса.Вставить("Действие", Действие);
	СтруктураЗапроса.Вставить("Справочник", Справочник);
	СтруктураЗапроса.Вставить("ПараметрыСтроки", ПараметрыСтроки);
	СтруктураЗапроса.Вставить("ПараметрыТела",	ПараметрыТела);
	
	Возврат СтруктураЗапроса;
	
КонецФункции

Функция ПолучитьПараметрыИзТела(Запрос)
	
	ТипСообщения = Запрос.Заголовки.Получить("Content-Type");
	
	СтруктураДанных = Новый Структура;
    СтруктураДанных.Вставить("ОшибкаЧтения", Ложь);

      //Разные браузеры по разному отдают "Content-Type","content-type", "Content-type"
	Если ТипСообщения = Неопределено тогда
		ТипСообщения = Запрос.Заголовки.Получить("content-type");
	КонецЕсли;

	Если ТипСообщения = Неопределено тогда
		ТипСообщения = Запрос.Заголовки.Получить("Сontent-type");
	КонецЕсли;
	
	Если ТипСообщения = Неопределено Тогда
		СтруктураДанных.Вставить("ОшибкаЧтения", Истина);
 		Возврат СтруктураДанных;
	КонецЕсли;
	
	Если Найти(Нрег(ТипСообщения), "multipart/form-data") > 0 тогда
		СтруктураДанных = ЗаполнитьСтруктуруДополнительныхПараметровИзТела(Запрос);
	ИначеЕсли Найти(Нрег(ТипСообщения), "application/json") > 0 тогда
		СтруктураДанных = ЗаполнитьСтруктуруДополнительныхПараметровJSON(Запрос);
	Иначе
		СтруктураДанных.Вставить("ОшибкаЧтения", Истина);
	КонецЕсли;
	
	Возврат СтруктураДанных;
	
КонецФункции

//С новыми методами для работы с JSON работать стало одно удовольствие. Даже с датами проблем нет.
Функция ЗаполнитьСтруктуруДополнительныхПараметровJSON(Запрос);
	ЧтениеJSON = Новый ЧтениеJSON;
	
	Данные = Запрос.ПолучитьТелоКакСтроку();
	
	ЧтениеJSON.УстановитьСтроку(Данные);
	Попытка
		СтруктураДанных = ПрочитатьJSON(ЧтениеJSON,,"sampleDate",ФорматДатыJSON.ISO);
		СтруктураДанных.Вставить("ОшибкаЧтения", Ложь);
	Исключение
		СтруктураДанных = Новый Структура;
		СтруктураДанных.Вставить("ОшибкаЧтения", Истина);
	КонецПопытки;	
	
	ЧтениеJSON.Закрыть();
	
	Возврат СтруктураДанных;
	
КонецФункции


В данном примере есть много вредных советов. Например ИмяСправочника, Действие необходимо переделать в перечисление или в новый справочник с соответствиями. Но как это элегантно реализовать в дополнении я еще не решил.

Разберем запрос getByParam. Полный адрес: /v1/catalogs/wares/getByParam?prop=byCode&comparison=similarly&searchString=239

Выясним с каким справочником мы работаем
Функция ОбработатьЗапросСправочник(СтруктураЗапроса)
	Если СтруктураЗапроса.Справочник = "Номенклатура" Тогда
		 СтруктураОтвета = ОбработатьЗапросНоменклатура(СтруктураЗапроса);
	ИначеЕсли СтруктураЗапроса.Справочник = "Склады" Тогда
		 СтруктураОтвета = ОбработатьЗапросСклады(СтруктураЗапроса)
	Иначе
		 СтруктураОтвета = ПодготовитьСтруктуруОтвета(100, "Не найден обработчик справочника " +  СтруктураЗапроса.Справочник);
	КонецЕсли;
	 
	Возврат СтруктураОтвета; 
КонецФункции


Получаем метод запроса. И проверяем что к методу прилагаются все параметры. Здесь надо отметить глубокую проверку я не делаю. Так как пишу и для 1С, и для Android.
Функция ОбработатьЗапросНоменклатура(СтруктураЗапроса)
	Если СтруктураЗапроса.Действие = "ПолучитьПоПараметрам" Тогда
		РеквизитСравнения = СтруктураЗапроса.ПараметрыСтроки.Получить("prop");
		ТипСравнения = СтруктураЗапроса.ПараметрыСтроки.Получить("comparison");
		СтрокаПоиска = СтруктураЗапроса.ПараметрыСтроки.Получить("searchString");
		
		
		Если (РеквизитСравнения = Неопределено) или (ТипСравнения = Неопределено) или (СтрокаПоиска = Неопределено) Тогда
			СтруктураОтвета = ПодготовитьСтруктуруОтвета(103, "Не заданы поля Реквизит сравнения и/или Тип сравнения или нет строки поиска");
			Возврат СтруктураОтвета;
		КонецЕсли;
		
		Данные  = НоменклатураВернутьЭлементыПоПараметрам(СтрокаПоиска, РеквизитСравнения, ТипСравнения);
		СтруктураОтвета = ПодготовитьСтруктуруОтвета(0, "");
		СтруктураОтвета.Вставить("payload", Данные);
		
	ИначеЕсли СтруктураЗапроса.Действие = "ПолучитьВсеЭлементы" Тогда 	
		Данные = НоменклатураВернутьВсеЭлементы();
		СтруктураОтвета = ПодготовитьСтруктуруОтвета(0, "");
		СтруктураОтвета.Вставить("payload", Данные);
			
	Иначе
		СтруктураОтвета = ПодготовитьСтруктуруОтвета("102", "Не наден обработчик для действия " + СтруктураЗапроса.Действие);
	КонецЕсли;
	
	Возврат СтруктураОтвета;
	
КонецФункции


Если все хорошо. Подготавливаем данные для ответа.
Функция НоменклатураВернутьЭлементыПоПараметрам(СтрокаПоиска, Реквизит, ТипСравнения)
			
	Запрос = Новый Запрос;
	
	ТекстЗапроса = 
	"ВЫБРАТЬ РАЗРЕШЕННЫЕ
	|	Номенклатура.Код КАК Код,
	|	Номенклатура.Артикул КАК Артикул,
	|	Номенклатура.Наименование КАК Наименование,
	|	Номенклатура.НаименованиеПолное КАК НаименованиеПолное,
	|	Номенклатура.ЕдиницаИзмерения.Наименование КАК ЕдиницаИзмеренияНаименование
	|ИЗ
	|	Справочник.Номенклатура КАК Номенклатура
	|ГДЕ
	|	НЕ Номенклатура.ПометкаУдаления
	|	И {Условие}";

		
	стрУсловие = "";
	
	Если НРег(Реквизит) = НРег("byCode") тогда
		стрУсловие = "Номенклатура.Код";
	ИначеЕсли НРег(Реквизит) = НРег("byArticle") тогда
		стрУсловие = "Номенклатура.Артикул";
	ИначеЕсли НРег(Реквизит) = НРег("byName") тогда
		стрУсловие = "Номенклатура.Наименование";
	КонецЕсли;
	
	Если НРег(ТипСравнения) = НРег("equally") тогда
		стрУсловие = стрУсловие +  " = &Параметр";
	ИначеЕсли НРег(ТипСравнения) = НРег("similarly") тогда
		стрУсловие = стрУсловие + " ПОДОБНО &Параметр";
	КонецЕсли;
	
	ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "{Условие}", стрУсловие);
		
		
	Запрос.Текст = ТекстЗапроса;
	
	Если НРег(ТипСравнения) = НРег("similarly") тогда
		СтрокаПоиска = "%" + СтрокаПоиска + "%";
	КонецЕсли;
	
	Запрос.УстановитьПараметр("Параметр", СтрокаПоиска); 	
		
	ТЗ = Запрос.Выполнить().Выгрузить();
		
	КолВоЭлементов = ТЗ.Количество();
	
	МассивЭлементов = Новый Массив;
	
	Для каждого СтрокаТЗ из ТЗ Цикл
		МассивЭлементов.Добавить(Новый Структура("code,article, name, fullName, unit", 
												СтрокаТЗ.Код, 
												СтрокаТЗ.Артикул,
												СтрокаТЗ.Наименование,
												СтрокаТЗ.НаименованиеПолное,
												СтрокаТЗ.ЕдиницаИзмеренияНаименование));			
	КонецЦикла;
	
	СтруктураДанных = Новый Структура;
	СтруктураДанных.Вставить("quantity", КолВоЭлементов);
	СтруктураДанных.Вставить("wares", МассивЭлементов);
	Возврат СтруктураДанных;
	
КонецФункции


И наконец объединяем все вместе. Помещаем это в JSON и отправляем ответ.
СтруктураОтвета = ПодготовитьСтруктуруОтвета(0, "");
СтруктураОтвета.Вставить("payload", Данные);
Возврат СтруктураОтвета;

Функция ПодготовитьОтвет(КодОтвета, СтруктураОтвета) Экспорт
	
	ТелоОтвета = ПолучитьСтрокуТелаОтвета(СтруктураОтвета);
	
	HTTPОтвет = Новый HTTPСервисОтвет(КодОтвета);
	HTTPОтвет.Заголовки["Content-Type"] = "application/json; charset=utf-8"; //сообщаем что это json UTF-8
	// ToDo CORS
	//HTTPОтвет.Заголовки["Access-Control-Allow-Origin"] = Оригин;
	//HTTPОтвет.Заголовки["Access-Control-Allow-Credentials"] = "true";
	HTTPОтвет.УстановитьТелоИзСтроки(ТелоОтвета);
	
	Возврат HTTPОтвет;
	
КонецФункции

Функция ПолучитьСтрокуТелаОтвета(СтруктураТела)
	
	ПараметрыJSON = Новый ПараметрыЗаписиJSON(, Символы.Таб,,,,,,,);
	
	ТелоОтвета = новый ЗаписьJSON;
	ТелоОтвета.ПроверятьСтруктуру = Ложь;
	ТелоОтвета.УстановитьСтроку(ПараметрыJSON);
	
	НастройкиСериализации = Новый НастройкиСериализацииJSON;
	НастройкиСериализации.ВариантЗаписиДаты = ВариантЗаписиДатыJSON.ЛокальнаяДата;
	НастройкиСериализации.ФорматСериализацииДаты = ФорматДатыJSON.ISO;
	
	ЗаписатьJSON(ТелоОтвета, СтруктураТела, НастройкиСериализации);
	
	СтрокаТелаОтвета = ТелоОтвета.Закрыть();

	Возврат СтрокаТелаОтвета;
	
КонецФункции


И отдаем это все на клиента.
	
Ответ = ПодготовитьОтвет(200, СтруктураОтвета)
Возврат Ответ;


Вне зависимости что мы получили от 1С. Всегда стараемся ответить правильно.
Функция ПодготовитьСтруктуруОтвета(КодОшибки, ТекстОшибки)
	СтруктураОтвета = Новый Структура;
	СтруктураРезультата = Новый Структура();
	СтруктураРезультата.Вставить("code", КодОшибки);
	СтруктураРезультата.Вставить("description", ТекстОшибки);
	СтруктураОтвета.Вставить("result", СтруктураРезультата);
	СтруктураОтвета.Вставить("payload", Новый Структура);
	
	Возврат СтруктураОтвета;
КонецФункции


На этом все. Все остальные методы реализованы похожим способом. Задавайте вопросы.С радостью отвечу.

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


  1. fishca
    29.10.2019 08:38

    С нумерацией частей у вас что-то не то. Это же только вторая часть, а не третья?
    И да в 8.3 сейчас сделали много полезных плюшек для интеграции с другими системами.


    1. loki82 Автор
      29.10.2019 08:41

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


  1. bvn13
    29.10.2019 10:19

    У меня для выдачи данных от 1С, завернутых в структуры и массивы был спец.модуль, который все типы данных сериализовал в JSON.


    Также для того, чтобы избежать съедания лицензий сервером 1С под каждого клиента такого API, была реализована программная авторизация с хранением данных сессии в регистре сведений. В Apache/IIS при этом был прописан конкретный один пользователь.


    1. loki82 Автор
      29.10.2019 10:47

      Тут будет ещё один цикл про golang и очереди. ReverseProxy, авторизация в 1С. И заворачивание 500 ошибки в json.


    1. Xilian
      29.10.2019 10:57

      >>завернутых в структуры и массивы был спец.модуль, который все типы данных сериализовал в JSON.

      Давно уже все в платформе.

      >>Также для того, чтобы избежать съедания лицензий сервером 1С под каждого клиента такого API

      HTTP/soap сервисы не едят клиентские лицензии. Они даже инициализацию не проводят.


      1. loki82 Автор
        29.10.2019 11:03

        Давно уже все в платформе.

        Не все. Но да, расширяют. Раньше только в XML. Теперь можно и в JSON. В любом случае такие вещи лучше обрабатывать ручками. И пакет меньше, и только нужные данные идут. Были времена когда между клиентом и сервером таблицу значений нужно было гонять через сериализацию. И вот в эти времена появились ОбщиеМодули с нужными процедурами.

        HTTP/soap сервисы не едят клиентские лицензии. Они даже инициализацию не проводят.

        Не едят правда. Но создают отдельное подключение с таймаутом. И в лицензировании четко написано, сколько активных клиентов(соединений), столько и лицензий. Инициализацию сеанса проводят.


      1. Neikist
        29.10.2019 14:03

        Давно уже все в платформе

        OData имеете ввиду? Отсутствует версионирование и контроль.


        1. o4karek
          31.10.2019 12:36

          Думаю, имеется ввиду работа с JSON.


    1. Neikist
      29.10.2019 14:02

      У меня для выдачи данных от 1С, завернутых в структуры и массивы был спец.модуль, который все типы данных сериализовал в JSON.

      Аналогично. Был реализован механизм который по имени объекта метаданных определял имя метода общего модуля который занимался сериализацией/десериализацией, и вся задача добавления новых объектов в api заключалась в описании двух функций (сериализация объекта в структуру и наоборот). Остальное механизм делал сам (формировал запросы, условия накладывал и прочее).
      А вот со своей авторизацией нужно осторожным быть. Особенно если пользователи api должны по RLS ограничиваться.


      1. loki82 Автор
        29.10.2019 14:10

        Мне не нравиться типовой механизм разграничений. Тем более разграничение по объектам завезли не так давно. Для этого был написан свой механизм, который на уровне middleware резал данные. Базовая авторизация использовалась только для reverseProxy. Тем самым сервис находится на VPS. А 1С скрыта от посторонних глаз. И даже если накосячить, и уронить платформу с 500 ошибкой. Все равно лишнего ничего не покажется пользователям. А клиенты были сайт на vueJS, в планах приложение на Android, и клиент на electron.


        1. Neikist
          29.10.2019 14:11

          Вроде да, вполне надежно, если не накосячить в middleware.