При разработке информационных систем на платформе 1С:Предприятие 8.3 часто возникает необходимость уведомить конкретного пользователя о наступлении важного события, произошедшего на сервере. Например, это может быть входящий HTTP-запрос, новый документ, изменение статуса процесса или другое событие, требующее немедленной реакции со стороны пользователя.
Перед нами стоит задача: серверная часть системы получает данные, предназначенные для конкретного пользователя. Если этот пользователь активен (работает в программе), мы должны немедленно оповестить его, желательно с возможностью обработки нажатия на уведомление, чтобы вызвать нашу клиентскую процедуру. Мы рассмотрим несколько подходов к решению этой задачи, от наиболее простых и рекомендуемых до более сложных, но гибких.
Система взаимодействия – это штатный механизм платформы 1С:Предприятие, специально разработанный для обмена сообщениями между пользователями и автоматизированными системами внутри одной или нескольких информационных баз, а также с внешними системами. Это наиболее простой и эффективный способ для нашей задачи, как справедливо было отмечено в обсуждении.
Преимущества Системы взаимодействия:
Реализация оповещения через Систему взаимодействия на сервере:
В серверном коде, где мы получаем входящий запрос и определяем целевого пользователя, нам необходимо сформировать объект сообщения и отправить его. Для этого используются методы общего модуля ОбсужденияУНФ (если он доступен в вашей конфигурации) или напрямую МенеджерСистемыВзаимодействия.
Рассмотрим пример кода, как это может выглядеть, используя структуру ДанныеСообщения:
&НаСервере
Процедура ОтправитьОповещениеЧерезСВ(СсылкаНаОбъект, ТекстСообщения, ПользовательПолучатель)
// Убедимся, что Система взаимодействия доступна.
// Если СВ не подключена или не настроена, этот код может выдать ошибку или не сработать.
Если Не ПодключенаСистемаВзаимодействия() Тогда
Возврат;
КонецЕсли;
// Создаем объект данных сообщения. В УНФ/КА/ERP есть вспомогательный общий модуль ОбсужденияУНФ.
// Если его нет, можно работать напрямую через МенеджерСистемыВзаимодействия.НовоеСообщение().
ДанныеСообщения = ОбсужденияУНФ.НовыйДанныеСообщения;
// Привязываем сообщение к конкретному объекту информационной базы.
// Это позволит пользователю перейти к объекту по клику на уведомление.
ДанныеСообщения.Объект = СсылкаНаОбъект; // Например, Справочники.События.НайтиПоКоду("ВходящийЗвонок") или вашДокумент.Ссылка
// Определяем текст сообщения, который увидит пользователь.
ДанныеСообщения.Текст = ТекстСообщения;
// Указываем дату и время сообщения.
ДанныеСообщения.Дата = ТекущаяДатаСеанса();
// Определяем автора сообщения. Это может быть системный пользователь или ассистент.
// Например, если есть предопределенный пользователь-ассистент:
Если ЗначениеЗаполнено(АссистентУправления.ПользовательАссистент) Тогда
ДанныеСообщения.Автор = АссистентУправления.ПользовательАссистент;
Иначе
ДанныеСообщения.Автор = Пользователи.ТекущийПользователь(); // Если нет специального автора, используем текущего
КонецЕсли;
// Указываем получателя сообщения. Это может быть элемент справочника 'Пользователи'.
ДанныеСообщения.Получатель = ПользовательПолучатель;
Попытка
// Создаем и отправляем сообщение.
ОбсужденияУНФ.СоздатьСообщение(ДанныеСообщения);
Исключение
ЗаписатьЛог("Ошибка отправки сообщения через Систему взаимодействия: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
// Вспомогательная функция для проверки доступности СВ (пример).
&НаСервере
Функция ПодключенаСистемаВзаимодействия()
Возврат ОбщегоНазначенияКлиентСервер.НачатьИспользоватьСистемуВзаимодействия();
КонецФункции
Если в вашей конфигурации нет модуля ОбсужденияУНФ или аналога, вы можете использовать непосредственно объекты Системы взаимодействия:
&НаСервере
Процедура ОтправитьОповещениеНапрямую(СсылкаНаОбъект, ТекстСообщения, ПользовательПолучатель)
Если Не МенеджерСистемыВзаимодействия.Подключена() Тогда
Возврат;
КонецЕсли;
// Создаем новое сообщение системы взаимодействия
Сообщение = МенеджерСистемыВзаимодействия.НовоеСообщение();
Сообщение.Текст = ТекстСообщения;
Сообщение.Получатели.Добавить(ПользовательПолучатель); // Добавляем получателя
// Прикрепляем объект к сообщению. При клике на уведомление откроется форма этого объекта.
Сообщение.Объекты.Добавить(СсылкаНаОбъект);
Попытка
Сообщение.Отправить();
Исключение
ЗаписатьЛог("Ошибка отправки сообщения через МенеджерСистемыВзаимодействия: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
Обработка оповещений на клиенте:
При получении сообщения через СВ, платформа самостоятельно выводит уведомление пользователю. Если в сообщении была прикреплена гиперссылка на объект (как в примере ДанныеСообщения.Объект = СсылкаНаОбъект;), при клике на уведомление будет открыта соответствующая форма объекта. Мы можем также перехватить событие получения сообщения в клиентском коде для выполнения более сложной логики. Для этого используются стандартные механизмы обработки оповещений, которые будут рассмотрены далее.
Важные нюансы:
ПользовательПолучатель является действительным элементом справочника Пользователи вашей информационной базы.Этот метод представляет собой классический подход "сервер пишет, клиент читает". Сервер записывает информацию об оповещении в специальный регистр сведений, а клиент периодически опрашивает этот регистр на наличие новых записей. Если новая запись найдена, клиент отображает уведомление.
Описание метода:
ПодключитьОбработчикОжидания), который с заданной периодичностью будет обращаться к серверу для проверки регистра сведений.Реализация на сервере (запись в Регистр сведений):
Сначала нам потребуется создать новый регистр сведений, например, НапоминанияПользователя. В нем могут быть такие измерения, как Пользователь (ссылка на справочник Пользователи) и Источник (ссылка на документ/справочник, который является источником напоминания), а также ресурсы ВремяСобытия, СрокНапоминания, Описание, Прочитано (булево).
Серверный код для записи в регистр будет выглядеть примерно так:
&НаСервере
Процедура ЗаписатьНапоминаниеВРегистр(ПользовательПолучатель, ИсточникОбъекта, ТекстНапоминания, СрокОтображения)
// Получаем менеджер регистра сведений.
МенеджерРС = РегистрыСведений.НапоминанияПользователя;
// Создаем новый набор записей.
// Можно использовать Отбор, чтобы не читать весь регистр, если записей много.
НаборЗаписей = МенеджерРС.СоздатьНаборЗаписей();
// Если нужно обновить существующую запись или добавить новую,
// можно сначала попытаться найти по отбору.
// В данном случае, просто добавляем новую.
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Пользователь = ПользовательПолучатель;
НоваяЗапись.Источник = ИсточникОбъекта;
НоваяЗапись.ВремяСобытия = ТекущаяДатаСеанса();
НоваяЗапись.СрокНапоминания = СрокОтображения; // Дата, до которой напоминание актуально
НоваяЗапись.Описание = ТекстНапоминания;
НоваяЗапись.Прочитано = Ложь; // Помечаем как непрочитанное
Попытка
НаборЗаписей.Записать();
ЗаписатьЛог("Запись напоминания в РС для пользователя " + Строка(ПользовательПолучатель) + ": " + ТекстНапоминания);
Исключение
ЗаписатьЛог("Ошибка записи напоминания в РС: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
Реализация на клиенте (опрос Регистра сведений с помощью обработчика ожидания):
Клиентский код должен периодически опрашивать регистр сведений. Это удобно делать в глобальном модуле или в модуле основной формы/рабочего стола.
Для этого нам понадобится клиентская функция, которая вызывает серверную функцию для чтения регистра, и процедура-обработчик ожидания.
// В каком-либо ОбщемМодуле (Клиент) или модуле формы рабочего стола
// Переменная для хранения последних напоминаний, чтобы не выводить одно и то же повторно
Перем мПоследниеНапоминанияНаКлиенте Экспорт; // Используем Экспорт, если переменная в общем модуле
// Процедура, вызываемая при начале работы системы или открытии основной формы
&НаКлиенте
Процедура ПриНачалеРаботыСистемыКлиент()
// Инициализация, если требуется
мПоследниеНапоминанияНаКлиенте = Новый Соответствие();
// Подключаем обработчик ожидания.
// "_ПроверитьНапоминания" - имя процедуры, 5 - интервал в секундах, Истина - повторять.
ПодключитьОбработчикОжидания("ПроверитьНапоминания", 5, Истина);
КонецПроцедуры
// Процедура-обработчик ожидания, которая будет вызываться каждые 5 секунд
&НаКлиенте
Процедура ПроверитьНапоминания()
// Вызываем серверную функцию для получения новых напоминаний
НовыеНапоминания = ПолучитьНовыеНапоминанияНаСервере();
Для Каждого Напоминание Из НовыеНапоминания Цикл
// Проверяем, не показывали ли мы это напоминание уже
Если мПоследниеНапоминанияНаКлиенте.Получить(Напоминание.УникальныйИдентификатор) = Неопределено Тогда
ПоказатьНапоминание(Напоминание);
мПоследниеНапоминанияНаКлиенте.Вставить(Напоминание.УникальныйИдентификатор, Истина);
// Отправляем на сервер информацию о том, что напоминание показано
// или что его можно пометить как прочитанное
ПометитьНапоминаниеПрочитаннымНаСервере(Напоминание.УникальныйИдентификатор);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
// Серверная функция для получения новых напоминаний
&НаСервере
Функция ПолучитьНовыеНапоминанияНаСервере()
Результат = Новый СписокЗначений();
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| НапоминанияПользователя.Ссылка,
| НапоминанияПользователя.Пользователь,
| НапоминанияПользователя.Описание,
| НапоминанияПользователя.Источник
|ИЗ
| РегистрСведений.НапоминанияПользователя КАК НапоминанияПользователя
|ГДЕ
| НапоминанияПользователя.Пользователь = &ТекущийПользователь
| И НапоминанияПользователя.Прочитано = ЛОЖЬ
| И НапоминанияПользователя.СрокНапоминания >= &ТекущаяДата";
Запрос.УстановитьПараметр("ТекущийПользователь", Пользователи.ТекущийПользователь());
Запрос.УстановитьПараметр("ТекущаяДата", ТекущаяДатаСеанса());
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
СтруктураНапоминания = Новый Структура;
СтруктураНапоминания.Вставить("УникальныйИдентификатор", Выборка.Ссылка.УникальныйИдентификатор());
СтруктураНапоминания.Вставить("Текст", Выборка.Описание);
СтруктураНапоминания.Вставить("Источник", Выборка.Источник);
Результат.Добавить(СтруктураНапоминания);
КонецЦикла;
Возврат Результат;
КонецФункции
// Серверная функция для пометки напоминания как прочитанного
&НаСервере
Процедура ПометитьНапоминаниеПрочитаннымНаСервере(УИДНапоминания)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| НапоминанияПользователя.Ссылка
|ИЗ
| РегистрСведений.НапоминанияПользователя КАК НапоминанияПользователя
|ГДЕ
| НапоминанияПользователя.Ссылка.УникальныйИдентификатор = &УИДНапоминания";
Запрос.УстановитьПараметр("УИДНапоминания", УИДНапоминания);
Результат = Запрос.Выполнить().Выбрать();
Если Результат.Следующий() Тогда
НаборЗаписей = РегистрыСведений.НапоминанияПользователя.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Ссылка.Установить(Результат.Ссылка);
НаборЗаписей.Прочитать();
Если НаборЗаписей.Количество() > 0 Тогда
НаборЗаписей[0].Прочитано = Истина;
НаборЗаписей.Записать();
КонецЕсли;
КонецЕсли;
КонецПроцедуры
// Клиентская процедура для отображения напоминания
&НаКлиенте
Процедура ПоказатьНапоминание(Напоминание)
// Сообщение пользователю
ПоказатьОповещениеПользователя(
Новый ОписаниеОповещения("ОбработкаНажатияНаНапоминание", ЭтотОбъект, Напоминание.Источник),
Напоминание.Текст
);
КонецПроцедуры
// Клиентская процедура для обработки нажатия на оповещение
&НаКлиенте
Процедура ОбработкаНажатияНаНапоминание(РезультатВыбора, ДополнительныеПараметры) Экспорт
Если ДополнительныеПараметры <> Неопределено И ТипЗнч(ДополнительныеПараметры) = Тип("Ссылка") Тогда
// Открываем форму объекта, на который ссылалось напоминание
ОткрытьЗначение(ДополнительныеПараметры);
КонецЕсли;
КонецПроцедуры
Важные нюансы метода с регистром сведений:
Этот подход является более низкоуровневым и требует детальной реализации логики взаимодействия. Он может быть полезен, если Система взаимодействия по каким-то причинам не подходит или требуется очень специфический механизм.
Описание метода:
Идея состоит в том, чтобы использовать параметр сеанса как флаг, сигнализирующий о новом событии. HTTP-сервис изменяет этот параметр на сервере, а клиентский обработчик ожидания или оповещения реагирует на изменение флага.
ЕстьНовоеОповещениеДляПользователя.Истина.Истина, вызываем открытие формы внешней обработки (или другой формы) с нашим сообщением. После отображения сообщения сбрасываем параметр сеанса в Ложь.Пример реализации (с использованием Оповестить и ОбработкаОповещения):
Этот вариант использует связку клиент-сервер, где серверная часть вызывает клиентскую процедуру через Оповестить. Обратите внимание, что Оповестить работает только в контексте, где есть клиент (например, в управляемой форме).
1. Общий модуль на сервере (например, ОбщийМодульОповещений):
// Общий модуль, например, "ОбщийМодульОповещений" (свойство "Сервер", "ВызовСерверногоМетода")
// Эта процедура будет вызвана с сервера.
// Она вызывает клиентскую процедуру Опов у всех активных клиентов.
&НаСервере
Процедура ОповеститьКлиентаССервера(ИмяСобытия, ПараметрСобытия, ИсточникСобытия) Экспорт
// Если нужно оповестить всех клиентов
// ОповеститьПользователей(ИмяСобытия, ПараметрСобытия, ИсточникСобытия);
// Если нужно оповестить конкретного клиента, это более сложно и обычно требует
// сохранения связи с клиентским сеансом или использования механизмов типа СВ.
// Для демонстрации Оповестить (в контексте, где есть клиент):
ОбщийМодульОповещенийКлиент.Опов(ИмяСобытия, ПараметрСобытия, ИсточникСобытия);
КонецПроцедуры
2. Общий модуль на клиенте (например, ОбщийМодульОповещенийКлиент):
// Общий модуль, например, "ОбщийМодульОповещенийКлиент" (свойство "Клиент", "ВызовСерверногоМетода")
// Эта процедура может быть вызвана с сервера (как в предыдущем примере)
// или из любой клиентской процедуры.
&НаКлиенте
Процедура Опов(ИмяСобытия, ПараметрСобытия, ИсточникСобытия) Экспорт
Сообщить("Получено оповещение: " + ИмяСобытия + ", " + ПараметрСобытия);
// Фактически, именно здесь происходит вызов системного механизма оповещения клиента.
Оповестить(ИмяСобытия, ПараметрСобытия, ИсточникСобытия);
КонецПроцедуры
// Переменная для хранения имени последнего события (для обработчика ожидания)
Перем _ИмяПоследнегоСобытия Экспорт;
// Глобальный обработчик оповещения
&НаКлиенте
Процедура ОбработкаОповещения(ИмяСобытия, Параметр, Источник) Экспорт
Сообщить("ОбработкаОповещения: " + ИмяСобытия + " " + Параметр + " "+ Источник);
// Сохраняем имя события для последующей обработки в обработчике ожидания
_ИмяПоследнегоСобытия = ИмяСобытия;
// Подключаем однократный обработчик ожидания, чтобы выполнить действия
// после завершения текущего цикла событий (например, чтобы не блокировать UI).
ПодключитьОбработчикОжидания("ПоказатьОбработанноеОповещение", 1, Ложь);
КонецПроцедуры
// Процедура, вызываемая обработчиком ожидания
&НаКлиенте
Процедура ПоказатьОбработанноеОповещение() Экспорт
Если ЗначениеЗаполнено(_ИмяПоследнегоСобытия) Тогда
Сообщить("Действие по оповещению: " + _ИмяПоследнегоСобытия);
// Здесь можно вызвать вашу обработку, открыть форму и т.п.
// Например: ОткрытьФорму("ОбщаяФорма.МоеОповещение", Новый Структура("Текст", _ИмяПоследнегоСобытия));
_ИмяПоследнегоСобытия = ""; // Сбрасываем, чтобы не показывать повторно
КонецЕсли;
КонецПроцедуры
3. Использование в документе (или HTTP-сервисе) на сервере:
// В обработчике события, например, ПриЗаписиНаСервере документа
&НаСервере
Процедура ПриЗаписиНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
// Допустим, нам нужно оповестить о записи этого документа.
// Можно передать ссылку на объект, текст сообщения и т.д.
ОбщийМодульОповещений.ОповеститьКлиентаССервера("ДокументЗаписан", ТекущийОбъект.Ссылка.УникальныйИдентификатор(), ТекущийОбъект.Ссылка);
КонецПроцедуры
Подробное описание функций:
Оповестить(ИмяСобытия, Параметр, Источник): Это клиентская функция, которая "бросает" событие в систему. Все активные клиентские формы, которые подписаны на глобальную процедуру ОбработкаОповещения, смогут его перехватить. Важно понимать, что Оповестить запускается на клиенте, поэтому серверный код должен вызвать клиентский код, который уже вызовет Оповестить.ОбработкаОповещения(ИмяСобытия, Параметр, Источник): Глобальная клиентская процедура (или процедура в модуле формы), которая автоматически вызывается платформой, когда кто-либо вызывает Оповестить. Здесь мы получаем информацию о событии и можем на нее отреагировать.ПодключитьОбработчикОжидания(ИмяПроцедуры, Интервал, Повторять): Клиентская функция, которая позволяет запланировать вызов процедуры ИмяПроцедуры через определенный Интервал секунд. Параметр Повторять определяет, будет ли процедура вызываться многократно (Истина) или однократно (Ложь). Это очень полезно для асинхронной обработки, чтобы не блокировать основной поток выполнения клиентского приложения.Ограничения этого метода:
ОповеститьПользователей оповещает всех активных пользователей. Чтобы оповестить конкретного пользователя, требуется более сложная логика на сервере для определения его текущего клиентского сеанса и направления оповещения только туда, что зачастую менее удобно, чем СВ. В приведенном примере ОбщийМодульОповещений.ОповеститьКлиентаССервера пытается вызвать клиентский метод, но это сработает только если клиентский модуль находится в общем модуле, доступном для серверного вызова, и серверный контекст как-то связан с активным клиентским сеансом. В реальных сценариях для оповещения конкретного клиента с сервера без опроса регистра сведений сложнее.Оповестить и ОбработкаОповещения может работать не всегда так же эффективно или может иметь свои особенности, по сравнению с тонким клиентом.В некоторых случаях, когда требуется крайне высокая производительность, масштабируемость или интеграция с другими системами, может быть оправдано использование внешних брокеров сообщений или серверов реального времени, таких как Centrifugo, RabbitMQ (например, через WebSocket по протоколу STOMP), Apache Kafka и другие.
Описание:
При этом подходе сервер 1С не оповещает клиентов напрямую, а отправляет сообщение внешнему сервису. Клиентская часть (например, веб-клиент 1С через JavaScript, или внешняя обработка, или отдельное мобильное приложение) подписывается на сообщения от этого внешнего сервиса.
Когда использовать:
Пример (Centrifugo):
Centrifugo – это Open Source сервер для обмена сообщениями в режиме реального времени, который работает через WebSocket, HTTP-стриминг и другие протоколы. Интеграция с ним выглядит следующим образом:
ВыполнитьВызывающийКлиентскийМетод() или Оповестить()) для отображения уведомления.Сложность:
Этот подход требует значительно больше усилий по настройке, разработке и поддержке инфраструктуры (установка и конфигурирование Centrifugo, разработка клиентских и серверных интеграционных компонент) по сравнению со встроенными механизмами 1С. Однако он обеспечивает максимальную гибкость и масштабируемость для крупномасштабных и высоконагруженных систем.
Мы рассмотрели несколько способов оповещения пользователя на клиенте из серверного кода 1С. Выбор конкретного метода зависит от требований вашей системы:
Оповестить могут быть использованы для очень специфических нужд, но часто сложнее в реализации и менее универсальны.Независимо от выбранного метода, всегда уделяйте внимание безопасности (аутентификация, права доступа) и ведите логи для упрощения отладки и аудита системы оповещений.