Как динамически создавать регламентные задания для элементов справочника и настраивать их расписание в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В практической разработке на платформе 1С часто возникает задача автоматизации процессов по индивидуальным расписаниям пользователей. Например, это может быть автоматическое создание задач для сотрудников, рассылка отчетов в определенные дни, запуск периодических проверок данных или проведение регулярных регламентных операций по конкретным складам или контрагентам.

Очевидное и интуитивное решение — предоставить пользователю возможность в карточке справочника настраивать собственное расписание, по которому система будет автоматически выполнять заданные действия. Однако здесь возникает несколько ключевых вопросов: как связать расписание конкретного элемента справочника с механизмом регламентных заданий платформы, как программно создавать эти задания и как передавать в них контекст (ссылку на элемент справочника)?

В данной статье мы подробно разберем два основных подхода к реализации этой задачи: классический подход с динамическим созданием регламентных заданий под каждый элемент справочника и современный оптимизированный паттерн «Диспетчер», который активно применяется в высоконагруженных системах и конфигурациях на базе БСП (Библиотеки Стандартных Подсистем).

Выясним причины, по которым следует выбирать тот или иной вариант, проанализируем примеры кода и разберем по шагам весь процесс настройки.

Важное примечание: Все примеры кода и механизмы ориентированы на работу в управляемом приложении на платформе 1С:Предприятие 8.3.


Подход 1. Программное создание индивидуальных регламентных заданий платформы

Этот метод основан на штатной возможности платформы 1С создавать несколько экземпляров регламентных заданий на базе одного и того же объекта метаданных, определенного в конфигураторе. При этом каждый экземпляр будет иметь свое собственное уникальное расписание, имя и индивидуальные параметры запуска.

Рассмотрим по шагам, как реализовать этот механизм.

Шаг 1. Создание шаблона регламентного задания в конфигураторе

Прежде всего, нам необходимо заложить основу в конфигураторе:

  1. Откроем конфигуратор и перейдем в ветку Общие -> Регламентные задания.
  2. Добавим новое регламентное задание (например, с именем ВыполнениеПериодическихЗадач).
  3. В свойствах созданного регламентного задания снимем флаг Предопределенное. Это критически важно, так как предопределенное задание создается системой автоматически в единственном экземпляре, а нам нужно создавать их динамически из кода.
  4. В поле Имя метода укажем экспортную процедуру общего модуля, которая будет выполнять полезную работу. Модуль должен быть серверным (с установленными флагами Сервер, Вызов сервера или выполняться в привилегированном режиме).

Шаг 2. Настройка формы справочника и вызов диалога расписания

В справочнике, из которого будут создаваться задачи, необходимо предусмотреть хранение расписания и уникального идентификатора связанного регламентного задания. Для этого добавим в справочник реквизит ИдентификаторРегламентногоЗадания с типом Строка (длина 36 символов, для хранения UUID задания).

Для удобства пользователя выведем на форму справочника кнопку настройки расписания и строковое поле (надпись), в которой будет отображаться понятное представление настроенного расписания.

Разберем код на клиенте, который открывает стандартный диалог настройки расписания платформы 1С:


&НаКлиенте
Процедура НастроитьРасписание(Команда)
    
    // Создаем диалог работы с расписанием
    Диалог = Новый ДиалогРасписанияРегламентногоЗадания(РасписаниеЗначение);
    
    // Открываем диалог в модальном режиме (или через описание оповещения для асинхронного режима)
    Если Диалог.ОткрытьМодально() Тогда
        
        РасписаниеЗначение = Диалог.Расписание;
        // Выводим понятное текстовое представление расписания пользователю
        Элементы.НадписьРасписание.Заголовок = Строка(РасписаниеЗначение);
        Модифицированность = Истина;
        
    КонецЕсли;
    
КонецПроцедуры

В данном примере переменная РасписаниеЗначение является реквизитом формы с типом Произвольный. В ней временно хранится объект типа РасписаниеРегламентногоЗадания во время работы пользователя с формой.

Шаг 3. Запись элемента справочника и программное создание/обновление регламентного задания

При записи элемента справочника нам необходимо проверить, создано ли уже регламентное задание для этого элемента. Если создано — обновим его расписание и параметры. Если нет — создадим новое и запишем его уникальный идентификатор в реквизит справочника.

Посмотрим на пример серверного кода, который решает эту задачу в модуле объекта справочника при записи:


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    // Выполняем код только на сервере под привилегированным режимом, 
    // так как для работы с регламентными заданиями требуются административные права
    УстановитьПривилегированныйРежим(Истина);
    
    ТекущееЗадание = Неопределено;
    
    // Попробуем найти существующее регламентное задание по сохраненному UUID
    Если ЗначениеЗаполнено(ИдентификаторРегламентногоЗадания) Тогда
        УИД = Новый УникальныйИдентификатор(ИдентификаторРегламентногоЗадания);
        ТекущееЗадание = РегламентныеЗадания.НайтиПоУникальномуИдентификатору(УИД);
    КонецЕсли;
    
    // Если задание не нашли или его еще не существовало, создаем новое
    Если ТекущееЗадание = Неопределено Тогда
        // "ВыполнениеПериодическихЗадач" - имя нашего РЗ из метаданных конфигуратора
        ТекущееЗадание = РегламентныеЗадания.СоздатьРегламентноеЗадание("ВыполнениеПериодическихЗадач");
    КонецЕсли;
    
    // Заполняем ключевые параметры регламентного задания
    ТекущееЗадание.Наименование = "Периодическая задача: " + СокрЛП(Наименование);
    ТекущееЗадание.Использование = ИспользоватьРасписание; // Булево поле на форме (включено/выключено)
    
    // Передаем расписание, настроенное пользователем
    // (предполагается, что объект Расписание хранится в реквизите объекта или передан с формы)
    Если РасписаниеОбъекта <> Неопределено Тогда
        ТекущееЗадание.Расписание = РасписаниеОбъекта;
    КонецЕсли;
    
    // Передаем контекст выполнения. Параметры РЗ принимают массив значений.
    // Первым параметром передаем ссылку на текущий элемент справочника.
    МассивПараметров = Новый Массив;
    МассивПараметров.Добавить(Ссылка);
    ТекущееЗадание.Параметры = МассивПараметров;
    
    // Записываем регламентное задание в базу данных
    Попытка
        ТекущееЗадание.Записать();
        // Сохраняем идентификатор созданного/обновленного РЗ в карточке справочника
        ИдентификаторРегламентногоЗадания = Строка(ТекущееЗадание.УникальныйИдентификатор);
    Исключение
        ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Не удалось записать регламентное задание: " + ОписаниеОшибки());
    КонецПопытки;
    
    УстановитьПривилегированныйРежим(Ложь);
    
КонецПроцедуры

Шаг 4. Обработка выполнения задания в общем модуле

Когда наступает время запуска по расписанию, платформа автоматически запускает фоновое задание и вызывает метод общего модуля, указанный нами на Шаге 1. В этот метод платформа автоматически передает параметры, которые мы упаковали в массив при создании задания.

Рассмотрим подробнее реализацию этого метода:


// Метод должен быть экспортным. Параметр "СсылкаНаНастройку" примет значение,
// которое мы добавили в массив параметров регламентного задания.
Процедура ВыполнитьПериодическуюЗадачу(СсылкаНаНастройку = Неопределено) Экспорт
    
    // Если по какой-то причине параметр не передан, прерываем выполнение
    Если СсылкаНаНастройку = Неопределено Тогда
        ЗаписьЖурналаРегистрации("РегламентныеЗадачи.Выполнение",
            УровеньЖурналаРегистрации.Предупреждение, , ,
            "Запуск регламентного задания произведен без указания ссылки на элемент настроек!");
        Возврат;
    КонецЕсли;
    
    // Проверим актуальность элемента справочника
    ОбъектНастройки = СсылкаНаНастройку.ПолучитьОбъект();
    Если ОбъектНастройки = Неопределено Или ОбъектНастройки.ПометкаУдаления Тогда
        Возврат;
    КонецЕсли;
    
    // Выполняем полезное действие на основе данных элемента справочника
    // Например, генерируем новую задачу для исполнителя
    НоваяЗадача = Задачи.ЗадачаИсполнителя.СоздатьЗадачу();
    НоваяЗадача.Дата = ТекущаяДата();
    НоваяЗадача.Наименование = "Регламентная задача: " + ОбъектНастройки.НаименованиеТематики;
    НоваяЗадача.Исполнитель = ОбъектНастройки.Исполнитель;
    НоваяЗадача.Описание = ОбъектНастройки.ОписаниеЗадачи;
    НоваяЗадача.Автор = ОбъектНастройки.Автор;
    
    Попытка
        НоваяЗадача.Записать();
        ЗаписьЖурналаРегистрации("РегламентныеЗадачи.Выполнение",
            УровеньЖурналаРегистрации.Информация, , СсылкаНаНастройку,
            "Успешно создана задача на основании настройки: " + Строка(СсылкаНаНастройку));
    Исключение
        ЗаписьЖурналаРегистрации("РегламентныеЗадачи.Выполнение",
            УровеньЖурналаРегистрации.Ошибка, , СсылкаНаНастройку,
            "Ошибка при создании задачи: " + ОписаниеОшибки());
    КонецПопытки;
    
КонецПроцедуры

Этот подход отлично работает, когда количество таких расписаний в базе невелико (в пределах нескольких десятков или первой сотни). Но что делать, если таких элементов справочника будут тысячи? Разберем ограничения данного метода и альтернативное решение.


Подход 2. Оптимизированный паттерн «Диспетчер» для высоконагруженных систем

Несмотря на простоту реализации первого подхода, у него есть существенные архитектурные недостатки, которые проявляются по мере роста информационной базы:

Для решения этих проблем профессиональные разработчики используют паттерн «Диспетчер». Суть паттерна заключается в следующем: создается ровно одно предопределенное регламентное задание, которое запускается с высокой периодичностью (например, раз в минуту). Это задание считывает настройки расписаний напрямую из справочника, определяет, какие из них должны быть запущены в текущую минуту, и инициирует их выполнение асинхронно.

Хранение расписания в реквизитах справочника

Поскольку мы отказываемся от создания платформенных регламентных заданий под каждую настройку, нам нужно научиться сохранять объект расписания прямо внутри элемента справочника. Объект РасписаниеРегламентногоЗадания поддерживает сериализацию, поэтому его можно сохранить в реквизит с типом ХранилищеЗначения или преобразовать в XML/JSON строку.

Проанализируем пример сохранения и восстановления расписания через механизм сериализации XML:


// Сохранение расписания в строковый реквизит справочника
Процедура СохранитьРасписаниеВЭлемент(ЭлементСправочника, ОбъектРасписание)
    
    ЗаписьXML = Новый ЗаписьXML;
    ЗаписьXML.УстановитьСтроку();
    
    // Сериализуем объект расписания в XML-строку
    СериализаторXDTO.ЗаписатьXML(ЗаписьXML, ОбъектРасписание, "Schedule", "http://v8.1c.ru/8.1/data/jobs");
    
    СтрокаXML = ЗаписьXML.Закрыть();
    ЭлементСправочника.СтрокаРасписанияXML = СтрокаXML;
    
КонецПроцедуры

// Восстановление расписания из строкового реквизита
Функция ВосстановитьРасписаниеИзЭлемента(ЭлементСправочника)
    
    Если Не ЗначениеЗаполнено(ЭлементСправочника.СтрокаРасписанияXML) Тогда
        Возврат Новый РасписаниеРегламентногоЗадания;
    КонецЕсли;
    
    ЧтениеXML = Новый ЧтениеXML;
    ЧтениеXML.УстановитьСтроку(ЭлементСправочника.СтрокаРасписанияXML);
    
    Попытка
        ОбъектРасписание = СериализаторXDTO.ПрочитатьXML(ЧтениеXML, Тип("РасписаниеРегламентногоЗадания"));
        Возврат ОбъектРасписание;
    Исключение
        Возврат Новый РасписаниеРегламентногоЗадания;
    КонецПопытки;
    
КонецФункции

Как диспетчер определяет необходимость запуска задачи

В платформе 1С у объекта РасписаниеРегламентногоЗадания есть замечательный, но малоизвестный метод — ТребуетсяВыполнение(). Этот метод принимает на вход проверяемую дату и возвращает Истина, если согласно настройкам расписания задача должна быть запущена в указанный момент времени.

Рассмотрим код главной процедуры Диспетчера задач, которая запускается один раз в минуту:


Процедура ДиспетчерПериодическихЗадач() Экспорт
    
    ТекущееВремя = ТекущаяДата();
    
    // Получаем все активные настройки из справочника задач
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   ПериодическиеЗадачи.Ссылка КАК Ссылка,
        |   ПериодическиеЗадачи.СтрокаРасписанияXML КАК СтрокаРасписанияXML
        |ИЗ
        |   Справочник.ПериодическиеЗадачи КАК ПериодическиеЗадачи
        |ГДЕ
        |   ПериодическиеЗадачи.Активна = ИСТИНА
        |   И ПериодическиеЗадачи.ПометкаУдаления = ЛОЖЬ";
        
    РезультатЗапроса = Запрос.Выполнить();
    Выборка = РезультатЗапроса.Выбрать();
    
    Пока Выборка.Следующий() Цикл
        
        // Восстанавливаем объект расписания для каждой настройки
        Расписание = ВосстановитьРасписаниеИзВыборки(Выборка.СтрокаРасписанияXML);
        
        // Проверяем, должна ли задача выполняться прямо сейчас
        Если Расписание.ТребуетсяВыполнение(ТекущееВремя) Тогда
            
            // Чтобы одна долгая задача не тормозила обработку остальных настроек,
            // запускаем каждую задачу асинхронно в фоновом задании
            НаименованиеПотока = "Выполнение задачи: " + Строка(Выборка.Ссылка);
            МассивПараметров = Новый Массив;
            МассивПараметров.Добавить(Выборка.Ссылка);
            
            ФоновыеЗадания.Выполнить(
                "ОбщийМодульВыполненияЗадач.ВыполнитьПериодическуюЗадачу", 
                МассивПараметров, 
                Новый УникальныйИдентификатор(), 
                НаименованиеПотока
            );
            
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

Преимущества паттерна «Диспетчер»:

  1. В списке регламентных заданий администратор видит всего одну аккуратную строку: Диспетчер периодических задач.
  2. Нагрузка на СУБД сводится к одному быстрому запросу в минуту.
  3. Система легко масштабируется на десятки тысяч индивидуальных расписаний пользователей без падения производительности платформы.

Гибридный метод: Оптимизация производительности при больших объемах данных

Если в вашей базе данных количество элементов справочника с настроенными расписаниями превышает несколько тысяч, даже паттерн «Диспетчер» в чистом виде может начать создавать пиковую нагрузку на сервер. Проблема кроется в необходимости десериализации XML-строк в объекты 1С внутри цикла. Чтение XML и десериализация объектов через СериализаторXDTO — относительно «тяжелая» операция для процессора сервера.

Чтобы избежать десериализации неактуальных задач, мы можем применить гибридный метод. Разберем его суть: самые базовые параметры расписания мы дублируем в простые реквизиты справочника в виде обычных примитивных типов данных (Булево, Число, Дата). Это позволит нам отсекать 99% заведомо неподходящих задач еще на уровне запроса к СУБД, вообще не прибегая к чтению XML.

Рекомендуется добавить в справочник следующие реквизиты-дублеры:

При записи элемента справочника мы программно анализируем объект расписания и заполняем эти реквизиты на основе свойств ДниНедели и ВремяНачала объекта РасписаниеРегламентногоЗадания.

Тогда запрос в нашем Диспетчере будет выглядеть гораздо эффективнее:


// Определяем текущий день недели
НомерДняНедели = ДеньНедели(ТекущаяДата()); // Возвращает число от 1 до 7

Запрос = Новый Запрос;
// Строим динамический текст запроса в зависимости от текущего дня недели
ТекстФильтраДня = "";
Если НомерДняНедели = 1 Тогда
    ТекстФильтраДня = "И ПериодическиеЗадачи.Пн = ИСТИНА";
ИначеЕсли НомерДняНедели = 2 Тогда
    ТекстФильтраДня = "И ПериодическиеЗадачи.Вт = ИСТИНА";
// и так далее для всех дней недели...
КонецЕсли;

Запрос.Текст = 
    "ВЫБРАТЬ
    |   ПериодическиеЗадачи.Ссылка КАК Ссылка,
    |   ПериодическиеЗадачи.СтрокаРасписанияXML КАК СтрокаРасписанияXML
    |ИЗ
    |   Справочник.ПериодическиеЗадачи КАК ПериодическиеЗадачи
    |ГДЕ
    |   ПериодическиеЗадачи.Активна = ИСТИНА
    |   И ПериодическиеЗадачи.ПометкаУдаления = ЛОЖЬ
    |   " + ТекстФильтраДня + "
    |   И ПериодическиеЗадачи.ВремяНачалаЗадачи <= &ТекущееВремя";
    
Запрос.УстановитьПараметр("ТекущееВремя", ТекущаяДата());

Благодаря такому фильтру СУБД мгновенно вернет только те несколько десятков задач, которые потенциально должны запуститься именно сегодня и именно в это время. И только для этого узкого круга задач мы выполним точную проверку методом ТребуетсяВыполнение().


Резюме и рекомендации по выбору архитектуры

Подведем итоги нашего исследования работы с расписаниями в 1С:

← На главную