Как правильно реализовать длительные операции и отправку почты при проведении документа в обычном приложении 1С?

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

При проектировании бизнес-логики в 1С:Предприятии разработчики часто сталкиваются с необходимостью выполнять сопутствующие действия в момент проведения документов. Классический пример — отправка уведомления клиенту или менеджеру по электронной почте сразу после того, как документ провелся. Физическая отправка письма через SMTP-сервер может занимать от 3 до 10 секунд в зависимости от скорости интернета, размера вложений и отзывчивости почтового сервера.

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

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


Главная техническая ловушка: Транзакции и видимость данных

Прежде чем перейти к написанию кода, необходимо проанализировать фундаментальную проблему, о которой очень часто забывают начинающие разработчики. Попытка запустить фоновое задание напрямую из процедуры ОбработкаПроведения или из подписки на событие проведения через ФоновыеЗадания.Выполнить(...) — это опасный антипаттерн.

Давайте разберем по шагам, почему это происходит:

  1. Когда пользователь проводит документ, платформа открывает транзакцию базы данных. Процедура ОбработкаПроведения выполняется строго внутри этой транзакции.
  2. Если мы вызываем ФоновыеЗадания.Выполнить() внутри транзакции, платформа мгновенно инициализирует новый сеанс на сервере 1С для выполнения фонового задания. Это абсолютно независимый поток со своим сетевым соединением к СУБД.
  3. Фоновое задание запускается и пытается прочитать данные проводимого документа (например, чтобы сформировать печатную форму и отправить ее по почте).
  4. Проблема грязного чтения: Поскольку основная транзакция проведения в первом сеансе еще не завершена (не зафиксирована), СУБД защищает данные. Запущенное фоновое задание либо не увидит документ вообще (если он создается впервые, возникнет ошибка «Объект не найден»), либо прочитает его старую версию до изменения реквизитов.
  5. Проблема взаимных блокировок (Deadlocks): Если фоновое задание попытается дождаться появления документа или принудительно заблокировать таблицы для чтения, возникнет классический deadlock между основным сеансом, который держит транзакцию проведения, и фоновым сеансом, пытающимся прочитать эти данные.
  6. Проблема отката транзакции: Если после запуска фонового задания в процессе проведения возникнет какая-либо ошибка (например, не хватило остатков на складе) и транзакция откатится, то фоновое задание, запущенное в середине процесса, всё равно выполнится и отправит письмо контрагенту, хотя документ по факту не провелся.

Как обойти эту проблему? Существует два архитектурно верных решения:


Способ 1. Запуск асинхронной операции через ФоновыеЗадания.Выполнить()

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

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


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

Теперь рассмотрим, как инициировать этот процесс. Если мы работаем в обычном приложении, вызов должен происходить в модуле формы документа в обработчике ПослеЗаписиНаСервере или ПослеЗаписи на клиенте с вызовом сервера:


Процедура ЗапуститьОтправкуПисьма(ДокументСсылка)
    ПараметрыМетода = Новый Массив;
    СтруктураПараметров = Новый Структура;
    СтруктураПараметров.Вставить("АдресСервера", "smtp.yandex.ru");
    СтруктураПараметров.Вставить("Порт", 465);
    СтруктураПараметров.Вставить("АдресПолучателя", "client@example.com");
    СтруктураПараметров.Вставить("Тема", "Ваш заказ № " + ДокументСсылка.Номер);
    СтруктураПараметров.Вставить("ТекстПисьма", "Здравствуйте! Ваш заказ успешно оформлен.");
    
    ПараметрыМетода.Добавить(СтруктураПараметров);
    
    // Запускаем фоновое задание без ожидания его завершения
    ФоновыеЗадания.Выполнить("ОбщегоНазначенияСервер.ОтправитьПисьмоФоновымЗаданием", ПараметрыМетода, , "Отправка письма по заказу " + ДокументСсылка.Номер);
КонецПроцедуры

Этот код мгновенно вернет управление пользователю. Письмо уйдет в параллельном потоке, не блокируя интерфейс программы.


Как отследить завершение фонового задания в обычной форме

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

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

  1. При запуске фонового задания на сервере мы получаем его уникальный идентификатор и возвращаем его на клиент.
  2. На клиенте в обычной форме запускаем периодический опрос состояния задания с помощью встроенного механизма платформы.

Рассмотрим пример реализации в модуле обычной формы:


Перем ИдентификаторЗадания;

Процедура При НажатииКнопкиВыполнить(Кнопка)
    // Запускаем операцию на сервере и получаем ID задания
    ИдентификаторЗадания = ЗапуститьДлительнуюОперациюНаСервере();
    
    // Подключаем опрос состояния каждые 2 секунды
    ПодключитьОбработчикОжидания("ПроверитьСтатусВыполнения", 2);
    ЭлементыФормы.ИндикаторВыполнения.Видимость = Истина; // Показываем анимацию загрузки
КонецПроцедуры

Процедура ПроверитьСтатусВыполнения() Экспорт
    Статус = ПолучитьСтатусЗаданияНаСервере(ИдентификаторЗадания);
    
    Если Статус = "Выполнено" Тогда
        ОтключитьОбработчикОжидания("ПроверитьСтатусВыполнения");
        ЭлементыФормы.ИндикаторВыполнения.Видимость = Ложь;
        Предупреждение("Операция успешно завершена!");
    ИначеЕсли Статус = "Ошибка" Тогда
        ОтключитьОбработчикОжидания("ПроверитьСтатусВыполнения");
        ЭлементыФормы.Indikator.Видимость = Ложь;
        ВызватьИсключение "В процессе выполнения длительной операции произошла ошибка!";
    КонецЕсли;
КонецПроцедуры

На сервере состояние фонового задания проверяется через менеджер фоновых заданий:


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

Способ 2. Надежная архитектура очереди через Регистр сведений

Прямой запуск фонового задания из формы решает проблему отзывчивости интерфейса, но абсолютно не защищает систему от инфраструктурных сбоев. Что произойдет, если в момент отправки письма на сервере временно пропадет интернет, отключится электричество или почтовый сервер выдаст ошибку авторизации SMTP Error: 503? Письмо просто исчезнет, а пользователь даже не узнает об этом.

Проанализируем классический отказоустойчивый подход с использованием очереди сообщений в базе данных:

  1. Создаем новый Регистр сведений ОчередьОтправкиПочты со следующей структурой:
    • Измерения: ИдентификаторСообщения (Тип: УникальныйИдентификатор или Строка).
    • Ресурсы: ДокументСсылка (ЛюбаяСсылка), АдресПолучателя (Строка), Тема (Строка), ТекстПисьма (Строка).
    • Реквизиты: КоличествоПопыток (Число), СтатусОтправки (ПеречислениеСсылка: Ожидает, Отправлено, Ошибка), ТекстОшибки (Строка), ДатаСоздания (Дата).
  2. В процедуре ОбработкаПроведения (или в подписке на событие) мы записываем намерение отправить письмо прямо в этот регистр. Так как это происходит в транзакции, запись гарантированно запишется только при успешном проведении документа. Если проведение откатится — запись в регистре тоже исчезнет.
  3. Создаем Регламентное задание, которое запускается по расписанию (например, каждую минуту) и обрабатывает записи со статусом Ожидает или Ошибка (если количество попыток меньше 5).

Рассмотрим пример кода обработки очереди регламентным заданием:


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

Преимущества данного подхода:


Способ 3. Адаптация БСП «Длительные операции» для обычного приложения

Если в вашей конфигурации на обычных формах интегрирована Библиотека стандартных подсистем (БСП), вы можете использовать ее мощные механизмы управления фоновыми задачами. Общий модуль БСП ДлительныеОперации содержит проверенный годами программный интерфейс.

Однако здесь существует техническое ограничение. Клиентские функции БСП (такие как ДлительныеОперацииКлиент) написаны с расчетом на управляемые формы. Они оперируют асинхронными вызовами и контекстом управляемой формы. В обычном приложении они напрямую не взлетят. Но мы можем задействовать серверную часть БСП!

Рассмотрим, как правильно организовать выполнение через БСП на сервере с ручным опросом на клиенте:


// На сервере запускаем фоновое задание через программный интерфейс БСП
Функция ЗапуститьДлительноеВыполнениеНаСервере(ПараметрыПроцедуры) Экспорт
    ИмяЭкспортируемойПроцедуры = "ОбщегоНазначенияСервер.ВыполнитьДлительнуюПроцедуру";
    
    ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияВФоне(Новый УникальныйИдентификатор);
    ПараметрыВыполнения.НаименованиеФоновогоЗадания = "Длительная операция БСП в обычном приложении";
    
    Результат = ДлительныеОперации.ЗапуститьВыполнениеВФоне(
                    ИмяЭкспортируемойПроцедуры, 
                    ПараметрыПроцедуры, 
                    ПараметрыВыполнения
                );
                
    Возврат Результат; // Возвращает структуру с Идентификатором задания и АдресомХранилища
КонецФункции

На клиенте в обычном приложении мы получаем структуру Результат, вытаскиваем из нее ИдентификаторЗадания и запускаем уже знакомый нам обработчик ожидания. На каждой итерации обработчика мы делаем серверный вызов для проверки состояния задания через функцию БСП:


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

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


Использование готовой подсистемы почты БСП

Если в вашей конфигурации есть БСП, нет необходимости вручную писать код работы с протоколом SMTP через объект ИнтернетПочта. В БСП встроена подсистема «Работа с почтовыми сообщениями».

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

Пример отправки сообщения через БСП выглядит чрезвычайно просто:


ПараметрыПисьма = Новый Структура;
ПараметрыПисьма.Вставить("Кому", "client@example.com");
ПараметрыПисьма.Вставить("Тема", "Уведомление о заказе");
ПараметрыПисьма.Вставить("Тело", "Ваш заказ №123 готов к отгрузке.");

// Отправка происходит асинхронно через встроенный механизм БСП
РаботаСПочтовымиСообщениями.ОтправитьПочтовоеСообщение(
    РаботаСПочтовымиСообщениями.СистемнаяУчетнаяЗапись(), 
    ПараметрыПисьма
);

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


Резюме и рекомендации

Подведем итоги нашего детального разбора. При выборе стратегии реализации длительных операций в обычном приложении 1С придерживайтесь следующих правил:

  1. Никогда не выполняйте ресурсоемкие внешние интеграции (почта, SMS, веб-сервисы, HTTP-запросы) непосредственно внутри транзакции проведения документа. Это прямой путь к блокировкам и потере данных.
  2. Для простых задач, где не критична 100% гарантия доставки (например, информационные уведомления), используйте моментальный запуск через клиентский вызов ФоновыеЗадания.Выполнить() в обработчике ПослеЗаписи.
  3. Для бизнес-критичных задач (отправка счетов клиентам, выгрузка накладных в государственные системы) всегда внедряйте архитектурный шаблон Очередь задач в Регистре сведений с последующей обработкой регламентным заданием. Это гарантирует доставку даже при падении серверов или отсутствии связи.
  4. Если в вашей системе присутствует БСП, задействуйте ее серверную часть (ДлительныеОперации и РаботаСПочтовымиСообщениями) — это сэкономит вам десятки часов разработки и тестирования.
← На главную