В процессе разработки в среде 1С:Предприятие программисты часто сталкиваются с задачей автоматического оповещения пользователей о тех или иных событиях. Например, когда необходимо напомнить менеджеру о запланированном звонке клиенту, вывести информацию о просроченных задачах или предупредить о подгрузке новых документов из внешних систем. Типичной ошибкой при реализации такого функционала является попытка вызвать интерактивные методы вывода сообщений напрямую из процедур регламентных заданий. В данной статье мы подробно разберем, почему этот подход принципиально не работает, проанализируем архитектурные ограничения платформы и рассмотрим несколько эффективных способов решения этой задачи: от классических обработчиков ожидания до современных push-уведомлений через Систему взаимодействия.
Для того чтобы понять суть проблемы, проанализируем, как именно платформа 1С выполняет регламентные задания. Любое регламентное задание запускается планировщиком и выполняется в рамках отдельного фонового задания на стороне сервера. Это накладывает два критических ограничения:
Сообщить(), ПоказатьОповещениеПользователя() или создать объект типа СообщениеПользователю внутри регламентного задания приводят к тому, что сообщения уходят "в никуда". Ошибки при этом не возникает, код завершается со статусом "Выполнено", но пользователь ничего не видит.ПараметрыСеанса.ТекущийПользователь (или его аналог в вашей конфигурации) не инициализирован. База данных обрабатывает этот сеанс как системный. Именно поэтому при принудительном ручном запуске задания из консоли регламентных заданий под вашей учетной записью код может отрабатывать корректно (так как инициализируется контекст вашего сеанса), а при автоматическом запуске по расписанию — выдавать пустой результат или ошибку получения текущего пользователя.Рассмотрим классический пример ошибочной реализации серверной процедуры общего модуля, которая привязывается к регламентному заданию:
Процедура ПроверитьНеобходимостьЗвонков() Экспорт
// ОШИБКА: Запрос пытается получить данные для ТекущегоПользователя,
// который не определен в автоматическом фоновом задании
ТекущийПользователь = ПараметрыСеанса.ТекущийПользователь;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Задачи.Клиент,
| Задачи.ВремяЗвонка
|ИЗ
| РегистрСведений.ЗадачиНаОбзвон КАК Задачи
|ГДЕ
| Задачи.Ответственный = &Ответственный
| И Задачи.Статус = ЗНАЧЕНИЕ(Перечисление.СтатусыЗадач.Активна)";
Запрос.УстановитьПараметр("Ответственный", ТекущийПользователь);
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
// ОШИБКА: Данное сообщение никогда не появится на экране пользователя
Сообщить("У вас есть невыполненные звонки клиентам!");
КонецЕсли;
КонецПроцедуры
Выяснив причину неработоспособности этого механизма, перейдем к рассмотрению корректных архитектурных шаблонов для реализации оповещений.
Если нам необходимо регулярно проверять наличие новых событий для конкретного пользователя и оперативно выводить их на экран, самым простым решением будет перенос инициативы опроса на сторону клиента. Для этого мы задействуем метод платформы ПодключитьОбработчикОжидания().
Этот метод регистрирует клиентскую процедуру, которая будет вызываться системой через заданный интервал времени. Давайте разберем по шагам, как это настроить в управляемом приложении.
Шаг 1. В модуле управляемого приложения (МодульУправляемогоПриложения) в процедуре ПриНачалеРаботыСистемы подключаем обработчик ожидания:
Процедура ПриНачалеРаботыСистемы()
// Подключаем обработчик ожидания с интервалом 60 секунд.
// Процедура "ПроверитьНапоминанияКлиент" должна быть расположена
// в неглобальном общем модуле с установленным флагом "Клиент (управляемое приложение)".
ПодключитьОбработчикОжидания("ОповещенияКлиент.ПроверитьНапоминанияКлиент", 60);
КонецПроцедуры
Шаг 2. Создаем общий модуль ОповещенияКлиент (свойства: Клиент, Вызов сервера) и пишем там обработчик:
Процедура ПроверитьНапоминанияКлиент() Экспорт
// Вызываем серверную функцию для получения списка актуальных напоминаний
СписокНапоминаний = ОповещенияСервер.ПолучитьАктуальныеНапоминанияДляПользователя();
Для Каждого Напоминание Из СписокНапоминаний Цикл
// Отображаем всплывающее окно на клиенте
ПоказатьОповещениеПользователя(
"Необходимо сделать звонок!",
,
Напоминание.ТекстОповещения,
БиблиотекаКартинок.Информация
);
КонецЦикла;
КонецПроцедуры
Шаг 3. В серверном общем модуле ОповещенияСервер (свойства: Сервер, Вызов сервера) реализуем выборку данных из базы под контекстом текущего сеанса пользователя:
Функция ПолучитьАктуальныеНапоминанияДляПользователя() Экспорт
ТекущийПользователь = Пользователи.АвторизованныйПользователь();
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Напоминания.ТекстНапоминания КАК ТекстОповещения
|ИЗ
| РегистрСведений.НапоминанияПользователей КАК Напоминания
|ГДЕ
| Напоминания.Пользователь = &Пользователь
| И Напоминания.ВремяОповещения <= &ТекущееВремя
| И НЕ Напоминания.Отправлено";
Запрос.УстановитьПараметр("Пользователь", ТекущийПользователь);
Запрос.УстановитьПараметр("ТекущееВремя", ТекущаяДата());
Результат = Запрос.Выполнить().Выгрузить();
// После получения данных помечаем их как отправленные, чтобы исключить повторное появление
Если Результат.Количество() > 0 Тогда
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.НапоминанияПользователей");
ЭлементБлокировки.УстановитьПараметр("Пользователь", ТекущийПользователь);
НачатьТранзакцию();
Попытка
Блокировка.Заблокировать();
Для Каждого Строка Из Результат Цикл
МенеджерЗаписи = РегистрСведений.НапоминанияПользователей.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Пользователь = ТекущийПользователь;
МенеджерЗаписи.ВремяОповещения = ТекущаяДата(); // или точное время из регистра
МенеджерЗаписи.Прочитать();
Если МенеджерЗаписи.Выбран() Тогда
МенеджерЗаписи.Отправлено = Истина;
МенеджерЗаписи.Записать();
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписатьЛогОшибки(ОписаниеОшибки());
КонецПопытки;
КонецЕсли;
Возврат Результат;
КонецФункции
Описанный выше подход имеет один существенный архитектурный недостаток — высокая нагрузка на СУБД. Представим, что в базе одновременно работают 100 пользователей. Если клиентское приложение каждого из них будет раз в минуту отправлять запрос к базе данных, это создаст до 6000 запросов в час. Если запрос сложный (например, анализирует табличные части документов или делает тяжелые соединения), это неизбежно приведет к замедлению работы всей системы и взаимным блокировкам.
Для минимизации этой нагрузки применим оптимизированную схему гибридного решения:
ОчередьНапоминаний.Пользователь и ресурсу Прочитано = Ложь. Такой запрос практически не нагружает СУБД.Спроектируем структуру регистра сведений ОчередьНапоминаний:
Пользователь (Тип: СправочникСсылка.Пользователи)ИдентификаторЗаписи (Тип: УникальныйИдентификатор)ТекстСообщения (Тип: Строка)Прочитано (Тип: Булево)Теперь процедура регламентного задания на сервере будет выглядеть так:
Процедура ЗагрузитьНапоминанияИзФайла() Экспорт
// Здесь располагается код загрузки данных из внешнего источника (например Excel)
ТаблицаДанных = ПолучитьДанныеИзФайлаЭкспорт();
Для Каждого СтрокаДанных Из ТаблицаДанных Цикл
// Находим нужного пользователя в системе
ЦелевойПользователь = Справочники.Пользователи.НайтиПоРеквизиту("ИдентификаторExcel", СтрокаДанных.КодСотрудника);
Если ЗначениеЗаполнено(ЦелевойПользователь) Тогда
// Создаем быструю запись в очередь оповещений
МенеджерЗаписи = РегистрСведений.ОчередьНапоминаний.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Пользователь = ЦелевойПользователь;
МенеджерЗаписи.ИдентификаторЗаписи = Новый УникальныйИдентификатор();
МенеджерЗаписи.ТекстСообщения = СтрокаДанных.ТекстНапоминания;
МенеджерЗаписи.Прочитано = Ложь;
МенеджерЗаписи.Записать(Истина);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Начиная с версии платформы 8.3.10 разработчикам доступен наиболее прогрессивный инструмент — Система взаимодействия. Это встроенный в платформу механизм обмена сообщениями, работающий по принципу push-уведомлений. При его использовании клиенту вообще не нужно запускать периодические обработчики ожидания и слать запросы к базе данных. Платформа держит открытым постоянное сетевое соединение с сервером Системы взаимодействия и мгновенно отображает уведомление на экране пользователя, как только оно было отправлено сервером.
Рассмотрим, как отправить оповещение из регламентного задания конкретному пользователю через этот механизм:
Процедура ОтправитьУведомлениеЧерезСистемуВзаимодействия(АдресатСсылка, ТекстОповещения) Экспорт
// Проверяем, подключена ли система взаимодействия в данной базе
Если Не СистемаВзаимодействия.Поддерживается() Тогда
Возврат;
КонецЕсли;
// Получаем пользователя системы взаимодействия по ссылке на справочник Пользователи
ПользовательСВ = СистемаВзаимодействия.ПолучитьПользователя(АдресатСсылка.УникальныйИдентификатор());
Если ПользовательСВ = Неопределено Тогда
// Если пользователь еще не зарегистрирован в системе взаимодействия
Возврат;
КонецЕсли;
// Создаем новое обсуждение (тет-а-тет) с пользователем или используем существующий канал интеграции
МассивУчастников = Новый Массив;
МассивУчастников.Добавить(ПользовательСВ);
ВыборкаОбсуждений = СистемаВзаимодействия.ВыбратьОбсуждения(Новый ОтборСистемыВзаимодействия(МассивУчастников));
Если ВыборкаОбсуждений.Следующий() Тогда
Обсуждение = ВыборкаОбсуждений.ПолучитьОбсуждение();
Иначе
Обсуждение = СистемаВзаимодействия.СоздатьОбсуждение();
Обсуждение.Участники.Добавить(ПользовательСВ);
Обсуждение.Наименование = "Системные напоминания";
Обсуждение.Записать();
КонецЕсли;
// Создаем и отправляем сообщение
НовоеСообщение = СистемаВзаимодействия.СоздатьСообщение(Обсуждение.Идентификатор);
НовоеСообщение.Текст = ТекстОповещения;
// Очень важно: так как фоновое задание выполняется под системной учетной записью,
// мы можем явно указать автора сообщения (например, сделать виртуального бота-помощника)
НовоеСообщение.Записать();
КонецПроцедуры
При таком подходе на стороне клиента в правом нижнем углу экрана у пользователя мгновенно появится стандартное всплывающее окно оповещения 1С с возможностью перехода в чат обсуждения. Это полностью решает проблему производительности базы данных. Для реализации подобного функционала можно использовать решение для отправки сообщений в Систему взаимодействия.
Если ваша конфигурация разработана с использованием Библиотеки стандартных подсистем (БСП), но в ней по какой-то причине отсутствует или отключена подсистема "Напоминания пользователя", оптимальным решением будет ее доинтеграция из стандартного дистрибутива БСП вашей версии.
Использование типовой подсистемы БСП дает вам следующие преимущества:
Пример программного создания напоминания средствами БСП из вашего регламентного задания:
Процедура СоздатьНапоминаниеСредствамиБСП(ПользовательСсылка, ТекстНапоминания, ОбъектИсточник) Экспорт
// Проверяем доступность общего модуля БСП
Если ОбщегоНазначения.ПодсистемаСуществует("СтандартныеПодсистемы.НапоминанияПользователя") Тогда
// Формируем структуру параметров для создания записи
ПараметрыНапоминания = Новый Структура;
ПараметрыНапоминания.Вставить("Пользователь", ПользовательСсылка);
ПараметрыНапоминания.Вставить("ВремяСобытия", ТекущаяДата() + 60); // напомнить через минуту
ПараметрыНапоминания.Вставить("СпособУстановкиВремени", Перечисления.СпособыУстановкиВремениНапоминания.В ОтношенииВремениИсточника);
ПараметрыНапоминания.Вставить("Текст", ТекстНапоминания);
ПараметрыНапоминания.Вставить("Источник", ОбъектИсточник);
// Вызываем программный интерфейс БСП
НапоминанияПользователяСлужебный.ЗаписатьНапоминание(ПараметрыНапоминания);
КонецЕсли;
КонецПроцедуры
В современных реалиях автоматизации бизнеса часто требуется, чтобы менеджер получил уведомление, даже если у него в данный момент закрыта программа 1С или он находится в пути. В таких случаях регламентное задание на сервере может отправлять оповещения во внешние мессенджеры, например, Telegram. Для настройки такого обмена подойдёт обработка для интеграции 1С с Telegram-ботом.
Для реализации этого метода нам не нужны никакие обработчики ожидания на стороне клиента 1С. Сервер регламентных заданий напрямую отправляет HTTP-запрос к API Telegram. Рассмотрим пример реализации функции отправки сообщения:
Процедура ОтправитьНапоминаниеВTelegram(ИдентификаторЧата, ТекстСообщения) Экспорт
ТокенБота = "123456789:AAF_ВашТокенБотаИзBotFather";
АдресРесурса = "api.telegram.org";
ПутьЗапроса = "bot" + ТокенБота + "/sendMessage";
// Формируем параметры JSON-запроса
ПараметрыЗапроса = Новый Структура;
ПараметрыЗапроса.Вставить("chat_id", ИдентификаторЧата);
ПараметрыЗапроса.Вставить("text", ТекстСообщения);
ПараметрыЗапроса.Вставить("parse_mode", "HTML");
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, ПараметрыЗапроса);
СтрокаЗапроса = ЗаписьJSON.Закрыть();
// Отправляем HTTP-запрос
HTTPСоединение = Новый HTTPСоединение(АдресРесурса, 443, , , , 10, Новый ЗащищенноеСоединениеOpenSSL());
HTTPЗапрос = Новый HTTPЗапрос(ПутьЗапроса);
HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/json");
HTTPЗапрос.УстановитьТелоИзСтроки(СтрокаЗапроса, КодировкаТекста.UTF8, ИспользованиеМаркераВремениПредставленияBOM.НеИспользовать);
Попытка
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
Если HTTPОтвет.КодСостояния <> 200 Тогда
ЗаписатьЛогОшибки("Ошибка отправки Telegram: Код ответа " + HTTPОтвет.КодСостояния);
КонецЕсли;
Исключение
ЗаписатьЛогОшибки("Не удалось выполнить HTTP запрос к Telegram: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
Для выбора оптимального архитектурного решения сопоставим рассмотренные варианты в таблице:
Выбрав наиболее подходящий под ваши задачи инструмент, вы сможете спроектировать надежную и производительную систему оповещения ваших пользователей, которая не будет замедлять работу базы данных.