В клиент-серверном режиме работы платформы «1С:Предприятие 8» все регламентные задания по умолчанию выполняются на сервере приложений под учетной записью служебного пользователя. Это логично и безопасно с точки зрения производительности системы. Однако разработчики часто сталкиваются с прикладными задачами, которые требуют интерактивного взаимодействия с пользователем в строго определенное время. Например, необходимо по расписанию открыть форму напоминания, запустить интерактивный отчет, обновить форму монитора руководителя или выполнить экспорт данных, требующий выбора файла на локальном диске пользователя.
Попытка вызвать клиентские методы (такие как ОткрытьФорму или Предупреждение) напрямую из регламентного задания на сервере приведет к ошибке выполнения, так как контекст сервера не имеет доступа к графическому интерфейсу пользователя (GUI). В этой статье мы подробно разберем, как обойти это ограничение платформы, детально проанализируем различные архитектурные подходы и выберем наиболее эффективное решение для вашей конфигурации.
Самое очевидное и простое решение, которое сразу приходит на ум — это использование стандартного метода платформы ПодключитьОбработчикОжидания. Его запускают при начале работы системы, и он с заданной периодичностью выполняет определенную процедуру.
Однако у этого метода в чистом виде есть существенные недостатки:
Ниже мы рассмотрим четыре профессиональных способа решения этой задачи, которые лишены данных недостатков.
---
Этот способ основан на хранении полноценного объекта расписания в информационной базе и периодической его проверке быстрым «легковесным» кодом на клиенте. Мы не привязываем интервал обработчика ожидания к моменту запуска сеанса. Вместо этого обработчик срабатывает достаточно часто (например, раз в минуту), но само полезное действие выполняется только тогда, когда текущее время совпадает с расписанием.
Разберем по шагам, как это реализовать.
Шаг 1. Создание интерфейса для настройки расписания. Мы можем предоставить администратору или пользователю возможность настраивать точное расписание с помощью стандартного диалога платформы. Для этого используется объект ДиалогРасписанияРегламентногоЗадания. Настроенное расписание мы сериализуем в XML-строку и сохраняем, например, в константу или специальный справочник.
// Процедура для вызова диалога настройки и сохранения расписания
Процедура НастроитьРасписаниеЗапуска() Экспорт
Диалог = Новый ДиалогРасписанияРегламентногоЗадания;
// Попытаемся получить ранее сохраненное расписание
ТекущееРасписаниеХранилище = Константы.РасписаниеКлиентскогоЗадания.Получить();
Если ТекущееРасписаниеХранилище <> Неопределено Тогда
Диалог.Расписание = ТекущееРасписаниеХранилище.Получить();
КонецЕсли;
Если Диалог.ОткрытьМодально() Тогда
// Сохраняем расписание в ИБ в виде Хранилища Значения
НовоеХранилище = Новый ХранилищеЗначения(Диалог.Расписание);
НачатьТранзакцию();
Попытка
Константы.РасписаниеКлиентскогоЗадания.Установить(НовоеХранилище);
ЗафиксироватьТранзакцию();
ПоказатьПредупреждение(, "Расписание успешно сохранено!");
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
КонецЕсли;
КонецПроцедуры
Шаг 2. Подключение обработчика ожидания на клиенте. В модуле управляемого приложения (или в модуле обычного приложения в процедуре ПриНачалеРаботыСистемы) мы подключаем обработчик, который будет срабатывать раз в 60 секунд. Это оптимальный интервал, который не создает нагрузки, но обеспечивает точность до минуты.
Процедура ПриНачалеРаботыСистемы()
// Подключаем проверку расписания раз в 60 секунд
ПодключитьОбработчикОжидания("ПроверитьНеобходимостьЗапускаПоРасписанию", 60);
КонецПроцедуры
Шаг 3. Логика проверки расписания. В глобальном модуле (или модуле управляемого приложения) описываем процедуру, которая будет вызываться обработчиком. Чтобы избежать лишних ресурсоемких серверных вызовов, мы проверяем условия максимально быстро.
Процедура ПроверитьНеобходимостьЗапускаПоРасписанию() Экспорт
#Если Клиент Тогда
// Вызываем серверную функцию для проверки расписания
Если НеобходимоЗапуститьДействиеНаСервере() Тогда
// Выполняем интерактивное действие на клиенте
ОткрытьФорму("Обработка.НапоминанияПользователю.Форма.ФормаУведомления");
КонецЕсли;
#КонецЕсли
КонецПроцедуры
Шаг 4. Серверная проверка расписания с использованием встроенного метода. Самый важный элемент этой схемы — использование метода СледующееВремяВыполнения() у объекта расписания. Этот метод позволяет рассчитать, когда задача должна запуститься относительно момента последней успешной проверки.
&НаСервереБезКонтекста
Функция НеобходимоЗапуститьДействиеНаСервере()
// Получаем настройки расписания
РасписаниеХранилище = Константы.РасписаниеКлиентскогоЗадания.Получить();
Если РасписаниеХранилище = Неопределено Тогда
Возврат Ложь;
КонецЕсли;
Расписание = РасписаниеХранилище.Получить();
Если Расписание = Неопределено Тогда
Возврат Ложь;
КонецЕсли;
ТекущееВремя = ТекущаяДата();
// Получаем дату последнего успешного выполнения из регистра сведений
ДатаПоследнегоВыполнения = ПолучитьДатуПоследнегоЗапуска(ПараметрыСеанса.ТекущийПользователь);
// Рассчитываем время следующего планового запуска
ВремяСледующегоЗапуска = Расписание.СледующееВремяВыполнения(ДатаПоследнегоВыполнения);
// Если время запуска наступило (или уже прошло, но мы его еще не обрабатывали)
Если ВремяСледующегоЗапуска <> Неопределено И ВремяСледующегоЗапуска <= ТекущееВремя Тогда
// Записываем факт выполнения, чтобы не сработало повторно на текущем шаге
ЗафиксироватьФактЗапуска(ПараметрыСеанса.ТекущийПользователь, ТекущееВремя);
Возврат Истина;
КонецЕсли;
Возврат Ложь;
ФункцияКонец
---
Если расписание очень сложное, или запуск завязан на тяжелые серверные вычисления (например, проведение документов, анализ остатков), лучшим решением будет использование серверного регламентного задания в паре с механизмом «очереди событий» в базе данных.
Суть метода:
Рассмотрим пример реализации этой схемы.
Создадим непериодический регистр сведений ОчередьКлиентскихСобытий со следующей структурой:
Пользователь (Тип: СправочникСсылка.Пользователи)ТипСобытия (Тип: Строка)ПараметрыСобытия (Тип: ХранилищеЗначения)Код серверного регламентного задания, которое формирует очередь:
Процедура ВыполнитьАнализИПодготовитьУведомления() Экспорт
// ... Какие-то сложные серверные расчеты ...
НужноУведомитьМенеджера = Истина;
ЦелевойПользователь = Справочники.Пользователи.НайтиПоРеквизиту("ИмяВхода", "ИвановИИ");
Если НужноУведомитьМенеджера И ЗначениеЗаполнено(ЦелевойПользователь) Тогда
// Создаем запись в регистре-очереди
Запись = РегистрыСведений.ОчередьКлиентскихСобытий.СоздатьМенеджерЗаписи();
Запись.Пользователь = ЦелевойПользователь;
Запись.ТипСобытия = "ОткрытьФормуКонтроляДолгов";
// Передаем параметры для будущей формы (например, массив должников)
СтруктураПараметров = Новый Структура("ДатаАнализа, КоличествоКонтрагентов", ТекущаяДата(), 5);
Запись.ПараметрыСобытия = Новый ХранилищеЗначения(СтруктураПараметров);
Запись.Записать(Истина); // Замещаем старую запись, если она не была обработана
КонецЕсли;
КонецПроцедуры
Теперь напишем клиентский обработчик ожидания, который будет считывать эту очередь:
Процедура ПриНачалеРаботыСистемы()
// Подключаем проверку очереди раз в 15-30 секунд
ПодключитьОбработчикОжидания("ПроверитьОчередьСобытий", 20);
КонецПроцедуры
Процедура ПроверитьОчередьСобытий() Экспорт
#Если Клиент Тогда
ПараметрыСобытия = Неопределено;
ТипСобытия = "ОткрытьФормуКонтроляДолгов";
// Получаем и сразу удаляем событие из базы данных за один серверный вызов
Если ПолучитьИУдалитьСобытиеИзОчереди(ТипСобытия, ПараметрыСобытия) Тогда
// Открываем форму и передаем в нее полученные с сервера параметры
ПараметрыФормы = Новый Структура;
ПараметрыФормы.Вставить("ДанныеСобытия", ПараметрыСобытия);
ОткрытьФорму("Обработка.МониторДолгов.Форма.ФормаОповещения", ПараметрыФормы);
КонецЕсли;
#КонецЕсли
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьИУдалитьСобытиеИзОчереди(Знач ТипСобытия, ПараметрыСобытия)
ТекущийПользователь = ПараметрыСеанса.ТекущийПользователь;
// Блокируем чтение и удаление в одной транзакции, чтобы избежать коллизий
НачатьТранзакцию();
Попытка
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Очередь.ПараметрыСобытия КАК ПараметрыСобытия
|ИЗ
| РегистрСведений.ОчередьКлиентскихСобытий КАК Очередь
|ГДЕ
| Очередь.Пользователь = &Пользователь
| И Очередь.ТипСобытия = &ТипСобытия";
Запрос.УстановитьПараметр("Пользователь", ТекущийПользователь);
Запрос.УстановитьПараметр("ТипСобытия", ТипСобытия);
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
// Извлекаем параметры
Если ЗначениеЗаполнено(Выборка.ПараметрыСобытия) Тогда
ПараметрыСобытия = Выборка.ПараметрыСобытия.Получить();
КонецЕсли;
// Удаляем запись из очереди, чтобы больше не обрабатывать
МенеджерЗаписи = РегистрыСведений.ОчередьКлиентскихСобытий.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Пользователь = ТекущийПользователь;
МенеджерЗаписи.ТипСобытия = ТипСобытия;
МенеджерЗаписи.Удалить();
ЗафиксироватьТранзакцию();
Возврат Истина;
КонецЕсли;
ОтменитьТранзакцию();
Возврат Ложь;
Исключение
ОтменитьТранзакцию();
ЗаписатьЛогОшибки(ОписаниеОшибки());
Возврат Ложь;
КонецПопытки;
ФункцияКонец
---
Если ваша конфигурация работает на платформе 8.3.10 или выше, и у вас развернут Сервер взаимодействия (Система взаимодействия), вы можете полностью отказаться от периодических опросов базы данных (пулинга) через обработчики ожидания.
Сервер взаимодействия использует технологию Push-уведомлений: клиентское приложение держит постоянное веб-сокет соединение с сервером. Как только на сервере генерируется событие, Сервер взаимодействия мгновенно отправляет пакет данных клиенту. Клиент реагирует на это событие мгновенно, без задержек.
Проанализируем программную реализацию этого современного подхода.
Шаг 1. Отправка сообщения с сервера (в контексте фонового/регламентного задания). Серверное задание создает специальное системное обсуждение и отправляет в него сообщение, адресованное конкретному пользователю системы взаимодействия.
Процедура ОтправитьPushУведомлениеПользователю(ИмяПользователя1С, ИмяСобытия, СтруктураДанных) Экспорт
// Получаем пользователя информационной базы
ПользовательИБ = ПользователиИнформационнойБазы.НайтиПоИмени(ИмяПользователя1С);
Если ПользовательИБ = Неопределено Тогда
Возврат;
КонецЕсли;
// Находим пользователя в системе взаимодействия
ИдентификаторПользователяСВ = СистемаВзаимодействия.ПолучитьИдентификаторПользователя(ПользовательИБ.УникальныйИдентификатор);
Если ИдентификаторПользователяСВ = Неопределено Тогда
Возврат; // Пользователь не зарегистрирован в Системе взаимодействия
КонецЕсли;
// Создаем неинтерактивное системное обсуждение (информационное)
МассивУчастников = Новый Массив;
МассивУчастников.Добавить(ИдентификаторПользователяСВ);
Обсуждение = СистемаВзаимодействия.СоздатьОбсуждение(МассивУчастников);
Обсуждение.Ключ = "СистемныеУведомления_" + ИмяПользователя1С;
Обсуждение.Назначение = НазначениеОбсуждения.Информационное;
Обсуждение.Записать();
// Создаем сообщение
Сообщение = СистемаВзаимодействия.СоздатьСообщение(Обсуждение.Идентификатор);
Сообщение.Текст = "Запуск клиентской задачи по расписанию";
// Сериализуем данные в ХранилищеЗначения для передачи на клиент
ДанныеДляПередачи = Новый Структура;
ДанныеДляПередачи.Вставить("ИмяСобытия", ИмяСобытия);
ДанныеДляПередачи.Вставить("Параметры", СтруктураДанных);
Сообщение.Данные = Новый ХранилищеЗначения(ДанныеДляПередачи);
Сообщение.Записать();
КонецПроцедуры
Шаг 2. Прием сообщения на стороне клиента. При запуске сеанса мы один раз регистрируем обработчик новых сообщений. Больше никаких периодических таймеров запускать не требуется!
Процедура ПриНачалеРаботыСистемы()
ПодключитьИнтеграциюССВ();
КонецПроцедуры
Процедура ПодключитьИнтеграциюССВ()
#Если Клиент Тогда
// Подключаем оповещение на новые входящие сообщения
ОписаниеОповещения = Новый ОписаниеОповещения("ПриПолученииСообщенияСВ", ЭтотОбъект);
СистемаВзаимодействия.ПодключитьОбработчикНовыхСообщений(ОписаниеОповещения);
#КонецЕсли
КонецПроцедуры
// Данная процедура сработает автоматически при поступлении push-сообщения
Процедура ПриПолученииСообщенияСВ(Сообщение, ДополнительныеПараметры) Экспорт
Если Сообщение.Данные <> Неопределено Тогда
ДанныеПакета = Сообщение.Данные.Получить();
Если ТипЗнч(ДанныеПакета) = Тип("Структура") Тогда
// Проверяем, наше ли это событие
Если ДанныеПакета.ИмяСобытия = "ОткрытьЭкранМониторинга" Тогда
ПараметрыОткрытия = Новый Структура;
ПараметрыОткрытия.Вставить("БыстрыйСтарт", Истина);
ОткрытьФорму("Обработка.МониторПродаж.Форма", ПараметрыОткрытия);
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
---
В некоторых случаях (например, ночной запуск ресурсоемких обработок, выгрузка логов, формирование тяжелых отчетов для печати на локальный принтер) не имеет смысла держать сеанс пользователя постоянно открытым и нагружать систему проверками. Для этого идеально подходит использование внешнего планировщика задач операционной системы (например, Windows Task Scheduler или cron в Linux).
Алгоритм работы следующий:
/C или /Execute.Пример строки запуска для планировщика Windows:
"C:\Program Files\1cv8\8.3.23.1688\bin\1cv8s.exe" ENTERPRISE /S "Server1C\Base1C" /N "РоботПечати" /P "StrongPassword" /C "ЗапуститьАвтопечать"
В модуле управляемого приложения в процедуре ПриНачалеРаботыСистемы мы перехватываем этот параметр и выполняем нужное действие:
Процедура ПриНачалеРаботыСистемы()
// Проверяем наличие параметра запуска командной строки
Если ЗначениеЗаполнено(ПараметрЗапуска) Тогда
Если ПараметрЗапуска = "ЗапуститьАвтопечать" Тогда
Попытка
// Открываем форму обработки и запускаем печать
ОбработкаОбъект = Обработки.АвтоматическаяПечатьДокументов.Создать();
ОбработкаОбъект.ВыполнитьПечатьНаЛокальныйПринтер();
Исключение
ЗаписатьЗаписьВЖурналРегистрации("ПечатьПоРасписанию",
УровеньЖурналаРегистрации.Ошибка,,, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
// Сразу после выполнения завершаем сеанс, чтобы не занимать лицензию
ЗавершитьРаботуСистемы(Ложь);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
---
Принудительное открытие модальных окон или обычных форм «поверх всех окон» по расписанию может сильно раздражать пользователей. Представьте ситуацию: бухгалтер сосредоточенно вводит длинное и важное платежное поручение, и в этот момент прямо посреди экрана открывается окно «Напоминание о необходимости заполнить отчет». Фокус ввода сбивается, пользователь случайно нажимает клавишу Space или Enter, закрывая ваше окно или, что еще хуже, отправляя недописанный документ.
Чтобы избежать подобных проблем и повысить лояльность пользователей к вашей системе автоматизации, придерживайтесь следующих правил:
ОткрытьФормуМодально). Это полностью блокирует работу всей системы до момента закрытия окна.ПоказатьОповещениеПользователя.Рассмотрим пример правильного вывода уведомления с возможностью клика по нему:
// Пример красивого и безопасного информирования пользователя
Процедура ПоказатьНеблокирующееУведомление(ДанныеСобытия) Export
ТекстЗаголовка = "Доступны новые данные!";
ТекстОповещения = "Нажмите здесь, чтобы открыть отчет по продажам.";
// Описываем действие, которое произойдет при клике на всплывающее окошко
ДействиеПриНажатии = Новый ОписаниеОповещения("ОткрытьФормуПродажПриКлике", ЭтотОбъект, ДанныеСобытия);
// Показываем аккуратное всплывающее окно в правом нижнем углу экрана
ПоказатьОповещениеПользователя(ТекстЗаголовка, ДействиеПриНажатии, ТекстОповещения, БиблиотекаКартинок.Информация);
КонецПроцедуры
Процедура ОткрытьФормуПродажПриКлике(ДополнительныеПараметры) Экспорт
// Этот код сработает ТОЛЬКО если пользователь сам кликнул по уведомлению
ОткрытьФорму("Обработка.МониторПродаж.Форма", ДополнительныеПараметры);
КонецПроцедуры
Такой подход делает работу пользователей с программой максимально комфортной: они получают оперативную информацию по расписанию, но при этом сами решают, когда им удобно прерваться и открыть нужный отчет или форму.