В практической работе разработчика 1С часто возникает задача массовой рассылки индивидуальных отчетов. Типичный пример: отправка показателей выручки по подразделениям на адреса электронной почты ответственных сотрудников. Настройки рассылки хранятся в регистре сведений с измерениями Подразделение и ЭлектроннаяПочта.
При реализации «в лоб» для каждого уникального email-адреса запускается формирование отдельного табличного документа. Если у организации сотни адресатов и десятки подразделений, сервер 1С и СУБД начинают испытывать колоссальную нагрузку, выполняя однотипную работу. Ситуация усложняется, когда у многих получателей совпадают наборы доступных подразделений, или когда действуют особые правила (например, если подразделение не заполнено — отправлять данные по всей компании).
В этой статье мы подробно разберем, как спроектировать элегантную и производительную систему рассылки, которая группирует адресатов с одинаковыми правами, формирует отчет один раз для группы и отправляет его всем участникам за один проход.
---
Сформулируем правила отбора данных, которые нам необходимо обработать:
Если выполнять эти проверки динамически в цикле обхода получателей, код быстро превратится в «спагетти». Давайте разберем по шагам, как решить эту задачу красиво и эффективно.
---
Прежде чем группировать адресатов, избавимся от проблемы избыточных строк (Правило 3) непосредственно в запросе. Нам нужно выявить адреса, у которых есть пустые подразделения, и пометить их как получателей «всего отчета», проигнорировав их частные записи.
Рассмотрим структуру запроса для нормализации связей:
// Временная таблица со всеми записями регистра
ВЫБРАТЬ
РегистрНастроек.ЭлектроннаяПочта КАК ЭлектроннаяПочта,
РегистрНастроек.Подразделение КАК Подразделение
ПОМЕСТИТЬ ВТ_ИсходныеНастройки
ИЗ
РегистрСведений.НастройкиРассылкиПоПодразделениям КАК РегистрНастроек;
////////////////////////////////////////////////////////////////////////////////
// Выявляем адреса, у которых есть запись с пустым подразделением (полный доступ)
ВЫБРАТЬ РАЗЛИЧНЫЕ
Настройки.ЭлектроннаяПочта КАК ЭлектроннаяПочта
ПОМЕСТИТЬ ВТ_АдресаСПолнымДоступом
ИЗ
ВТ_ИсходныеНастройки КАК Настройки
ГДЕ
Настройки.Подразделение ЕСТЬ NULL;
////////////////////////////////////////////////////////////////////////////////
// Формируем чистую таблицу связей
ВЫБРАТЬ
Настройки.ЭлектроннаяПочта КАК ЭлектроннаяПочта,
ВЫБОР
КОГДА ПолныйДоступ.ЭлектроннаяПочта НЕ ЕСТЬ NULL ТОГДА ЗНАЧЕНИЕ(Справочник.Подразделения.ПустаяСсылка)
ИНАЧЕ Настройки.Подразделение
КОНЕЦ КАК Подразделение
ПОМЕСТИТЬ ВТ_НормализованныеНастройки
ИЗ
ВТ_ИсходныеНастройки КАК Настройки
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_АдресаСПолнымДоступом КАК ПолныйДоступ
ПО Настройки.ЭлектроннаяПочта = ПолныйДоступ.ЭлектроннаяПочта;
Благодаря этому запросу мы получаем чистую таблицу ВТ_НормализованныеНастройки, где для адресатов с полным доступом осталась только одна строка с пустой ссылкой подразделения, а частные строки автоматически отброшены. Это решает проблему Правила 3 на уровне СУБД.
---
Теперь перед нами стоит ключевой вопрос: как понять, что у нескольких адресатов совпадает набор подразделений? Решать задачу перебором и сравнением массивов в цикле крайне нерационально.
Наиболее надежный и масштабируемый способ — вычисление строкового хэша отсортированного списка подразделений. Идея заключается в следующем:
Соответствие, где ключом выступает этот строковый хэш, а значением — массив подходящих email-адресов.Давайте посмотрим на практическую реализацию этого алгоритма на встроенном языке 1С:
// Получаем нормализованные данные из временной таблицы
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Настройки.ЭлектроннаяПочта КАК ЭлектроннаяПочта,
| Настройки.Подразделение КАК Подразделение,
| Настройки.Подразделение.УникальныйИдентификатор КАК ИДПодразделения
|ИЗ
| ВТ_НормализованныеНастройки КАК Настройки
|УПОРЯДОЧИТЬ ПО
| Настройки.ЭлектроннаяПочта,
| ИДПодразделения"; // Сортировка ИД критически важна для одинакового хэша!
ТаблицаДанных = Запрос.Выполнить().Выгрузить();
// Соответствие для группировки: Ключ - СтрокаХэша, Значение - Структура(МассивАдресов, МассивПодразделений)
ГруппыРассылки = Новый Соответствие;
ТекущийАдрес = "";
ТекущийМассивПодразделений = Новый Массив;
ТекущийМассивСтрокИД = Новый Массив;
Для Каждого СтрокаДанных Из ТаблицаДанных Цикл
Если СтрокаДанных.ЭлектроннаяПочта <> ТекущийАдрес Тогда
// Записываем накопленные данные для предыдущего адреса
Если НЕ ПустаяСтрока(ТекущийАдрес) Тогда
ДобавитьАдресВГруппу(ГруппыРассылки, ТекущийАдрес, ТекущийМассивПодразделений, ТекущийМассивСтрокИД);
КонецЕсли;
// Сброс буферов для нового адреса
ТекущийАдрес = СтрокаДанных.ЭлектроннаяПочта;
ТекущийМассивПодразделений = Новый Массив;
ТекущийМассивСтрокИД = Новый Массив;
КонецЕсли;
ТекущийМассивПодразделений.Добавить(СтрокаДанных.Подразделение);
ТекущийМассивСтрокИД.Добавить(СтрокаДанных.ИДПодразделения);
КонецЦикла;
// Не забываем обработать последний адрес после окончания цикла
Если НЕ ПустаяСтрока(ТекущийАдрес) Тогда
ДобавитьАдресВГруппу(ГруппыРассылки, ТекущийАдрес, ТекущийМассивПодразделений, ТекущийМассивСтрокИД);
КонецЕсли;
Ниже представлена вспомогательная процедура ДобавитьАдресВГруппу, которая вычисляет строковый хэш и формирует группы рассылки:
Процедура ДобавитьАдресВГруппу(ГруппыРассылки, АдресПочты, МассивПодразделений, МассивСтрокИД)
// Сортируем строковые идентификаторы, чтобы гарантировать совпадение хэшей
МассивСтрокИД.Сортировать();
ХэшНабора = СтрСоединить(МассивСтрокИД, "_");
Группа = ГруппыРассылки.Получить(ХэшНабора);
Если Группа = Неопределено Тогда
Группа = Новый Структура;
Группа.Вставить("Адреса", Новый Массив);
Группа.Вставить("Подразделения", МассивПодразделений);
ГруппыРассылки.Вставить(ХэшНабора, Группа);
КонецЕсли;
Группа.Адреса.Добавить(АдресПочты);
КонецПроцедуры
В результате работы данного алгоритма мы получаем готовую коллекцию групп. Каждый элемент коллекции содержит список уникальных подразделений (по которым нужно построить всего один отчет) и массив email-адресов получателей, которым данный отчет предназначается.
---
В ходе обсуждения на форуме (сообщение 15) был предложен интересный математический способ расчета хэша непосредственно в запросе с использованием степеней двойки. Давайте разберем, как устроен этот метод, его достоинства и критические недостатки.
Суть метода заключается в присвоении каждому подразделению уникального порядкового номера $N$, а в качестве хэш-значения подразделения берется число $2^N$. В запросе выполняется группировка по адресу электронной почты, а хэшем набора подразделений становится сумма этих значений (битовая маска):
ВЫБРАТЬ
Настройки.ЭлектроннаяПочта КАК ЭлектроннаяПочта,
СУММА(Настройки.БитовыйКодПодразделения) КАК ХэшНабора
ИЗ
ВТ_НормализованныеНастройки КАК Настройки
ГРУППИРОВАТЬ ПО
Настройки.ЭлектроннаяПочта;
Преимущества метода:
Критические ограничения:
Число в 1С имеет максимальную разрядность 38 знаков. Это означает, что метод применим, только если общее число подразделений в компании не превышает 120-125 единиц. При превышении этого лимита возникнет арифметическое переполнение СУБД.Вывод: этот метод красив с теоретической точки зрения, но опасен для крупных внедрений со сложной и динамично меняющейся структурой компании.
---
Имея сформированное соответствие групп рассылки, мы можем запустить итоговый цикл генерации и отправки почты. Обратите внимание на важную деталь: при отправке письма группе получателей адреса необходимо помещать в поле СкрытаяКопия (Bcc), чтобы получатели не видели чужие email-адреса из соображений конфиденциальности.
Для Каждого ЭлементГруппы Из ГруппыРассылки Цикл
ПараметрыГруппы = ЭлементГруппы.Значение;
// 1. Формируем табличный документ по массиву подразделений группы
ТабДок = СформироватьОтчетПоВыручке(ПараметрыГруппы.Подразделения);
// 2. Подготавливаем почтовое сообщение
Письмо = Новый ИнтернетПочтовоеСообщение;
Письмо.Тема = "Выручка по подразделениям от " + Формат(ТекущаяДата(), "ДФ=dd.MM.yyyy");
Письмо.Текст = "Добрый день! Во вложении отчет по выручке за отчетный период.";
// Добавляем сформированный отчет во вложения
ВременныйФайл = ПолучитьИмяВременногоФайла("xlsx");
ТабДок.Записать(ВременныйФайл, ТипФайлаТабличногоДокумента.XLSX);
Письмо.Вложения.Добавить(ВременныйФайл, "Выручка.xlsx");
// Добавляем всех адресатов группы в СКРЫТУЮ копию
Для Каждого АдресИз ГруппыПараметры.Адреса Цикл
Адресат = Письмо.СкрытыеПолучатели.Добавить();
Адресат.Адрес = АдресИз;
КонецЦикла;
// 3. Отправляем письмо через системную учетную запись
// ИнтернетПочта.Отправить(Письмо);
// Не забываем удалить временный файл
УдалитьФайлы(ВременныйФайл);
КонецЦикла;
---
В сообщении 19 была предложена еще одна интересная архитектурная идея: сформировать один гигантский табличный документ по всем подразделениям сразу, а затем программно «нарезать» его на части для каждого адресата, копируя области строк.
Давайте проанализируем эту возможность по шагам:
// Пример фиксации координат областей
ГраницыПодразделений.Вставить(ТекущееПодразделение, Новый Структура("Начало, Конец", ИсходныйТабДок.ВысотаТаблицы + 1, КонечнаяВысота));
ТабличныйДокумент и методом ПолучитьОбласть() копируем нужные строки из общего отчета, собирая индивидуальный документ.Преимущества метода:
Сложности реализации, о которых нужно помнить:
Вывести() или ВставитьОбласть() для копирования строк «съезжают» индивидуальные настройки ширины колонок. Вам придется вручную программно обходить колонки нового документа и устанавливать их ширину равной ширине колонок исходного документа:
Для НомерКолонки = 1 По ИсходныйТабДок.ШиринаТаблицы Цикл
НовыйТабДок.Область(, НомерКолонки).ШиринаКолонки = ИсходныйТабДок.Область(, НомерКолонки).ШиринаКолонки;
КонецЦикла;
---
Мы проанализировали несколько отличных архитектурных подходов к оптимизации массовой рассылки в среде «1С:Предприятие».
Для большинства практических задач наиболее надежным и безопасным решением является Шаг 1 и Шаг 2 (группировка по строковому хэшу списка подразделений). Этот подход: