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

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

В практической работе разработчика 1С часто возникает задача массовой рассылки индивидуальных отчетов. Типичный пример: отправка показателей выручки по подразделениям на адреса электронной почты ответственных сотрудников. Настройки рассылки хранятся в регистре сведений с измерениями Подразделение и ЭлектроннаяПочта.

При реализации «в лоб» для каждого уникального email-адреса запускается формирование отдельного табличного документа. Если у организации сотни адресатов и десятки подразделений, сервер 1С и СУБД начинают испытывать колоссальную нагрузку, выполняя однотипную работу. Ситуация усложняется, когда у многих получателей совпадают наборы доступных подразделений, или когда действуют особые правила (например, если подразделение не заполнено — отправлять данные по всей компании).

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

---

Анализ исходных правил и бизнес-логики

Сформулируем правила отбора данных, которые нам необходимо обработать:

  1. Правило «Все подразделения»: если в регистре для адресата указана строка с незаполненным (пустым) значением подразделения, этот адресат должен получить выручку по абсолютно всем подразделениям компании.
  2. Правило «Выбранные подразделения»: если для адресата указаны конкретные подразделения, он должен получить отчет только по ним.
  3. Приоритет полного доступа (Правило 3): если для одного и того же адреса в регистре есть как строка с пустым подразделением, так и строки с конкретными подразделениями, то пустая строка «поглощает» остальные. Адресат получает общий отчет по всей компании.

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

---

Шаг 1. Предварительная нормализация данных в запросе

Прежде чем группировать адресатов, избавимся от проблемы избыточных строк (Правило 3) непосредственно в запросе. Нам нужно выявить адреса, у которых есть пустые подразделения, и пометить их как получателей «всего отчета», проигнорировав их частные записи.

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


// Временная таблица со всеми записями регистра
ВЫБРАТЬ
	РегистрНастроек.ЭлектроннаяПочта КАК ЭлектроннаяПочта,
	РегистрНастроек.Подразделение КАК Подразделение
ПОМЕСТИТЬ ВТ_ИсходныеНастройки
ИЗ
	РегистрСведений.НастройкиРассылкиПоПодразделениям КАК РегистрНастроек;

////////////////////////////////////////////////////////////////////////////////
// Выявляем адреса, у которых есть запись с пустым подразделением (полный доступ)
ВЫБРАТЬ РАЗЛИЧНЫЕ
	Настройки.ЭлектроннаяПочта КАК ЭлектроннаяПочта
ПОМЕСТИТЬ ВТ_АдресаСПолнымДоступом
ИЗ
	ВТ_ИсходныеНастройки КАК Настройки
ГДЕ
	Настройки.Подразделение ЕСТЬ NULL;

////////////////////////////////////////////////////////////////////////////////
// Формируем чистую таблицу связей
ВЫБРАТЬ
	Настройки.ЭлектроннаяПочта КАК ЭлектроннаяПочта,
	ВЫБОР
		КОГДА ПолныйДоступ.ЭлектроннаяПочта НЕ ЕСТЬ NULL ТОГДА ЗНАЧЕНИЕ(Справочник.Подразделения.ПустаяСсылка)
		ИНАЧЕ Настройки.Подразделение
	КОНЕЦ КАК Подразделение
ПОМЕСТИТЬ ВТ_НормализованныеНастройки
ИЗ
	ВТ_ИсходныеНастройки КАК Настройки
	ЛЕВОЕ СОЕДИНЕНИЕ ВТ_АдресаСПолнымДоступом КАК ПолныйДоступ
		ПО Настройки.ЭлектроннаяПочта = ПолныйДоступ.ЭлектроннаяПочта;

Благодаря этому запросу мы получаем чистую таблицу ВТ_НормализованныеНастройки, где для адресатов с полным доступом осталась только одна строка с пустой ссылкой подразделения, а частные строки автоматически отброшены. Это решает проблему Правила 3 на уровне СУБД.

---

Шаг 2. Группировка получателей по хэшу набора подразделений

Теперь перед нами стоит ключевой вопрос: как понять, что у нескольких адресатов совпадает набор подразделений? Решать задачу перебором и сравнением массивов в цикле крайне нерационально.

Наиболее надежный и масштабируемый способ — вычисление строкового хэша отсортированного списка подразделений. Идея заключается в следующем:

  1. Для каждого адреса электронной почты получаем список его подразделений.
  2. Сортируем этот список по уникальному идентификатору (GUID) подразделения, чтобы порядок элементов всегда был строго одинаковым независимо от порядка выборки из базы.
  3. Объединяем GUID-ы в одну строку через разделитель. Полученная строка и будет уникальным хэшем (ключом) данного набора подразделений.
  4. Группируем адреса электронной почты в коллекцию Соответствие, где ключом выступает этот строковый хэш, а значением — массив подходящих email-адресов.

Давайте посмотрим на практическую реализацию этого алгоритма на встроенном языке 1С:


// Получаем нормализованные данные из временной таблицы
Запрос = Новый Запрос;
Запрос.Текст = 
	"ВЫБРАТЬ
	|	Настройки.ЭлектроннаяПочта КАК ЭлектроннаяПочта,
	|	Настройки.Подразделение КАК Подразделение,
	|	Настройки.Подразделение.УникальныйИдентификатор КАК ИДПодразделения
	|ИЗ
	|	ВТ_НормализованныеНастройки КАК Настройки
	|УПОРЯДОЧИТЬ ПО
	|	Настройки.ЭлектроннаяПочта,
	|	ИДПодразделения"; // Сортировка ИД критически важна для одинакового хэша!

ТаблицаДанных = Запрос.Выполнить().Выгрузить();

// Соответствие для группировки: Ключ - СтрокаХэша, Значение - Структура(МассивАдресов, МассивПодразделений)
ГруппыРассылки = Новый Соответствие;

ТекущийАдрес = "";
ТекущийМассивПодразделений = Новый Массив;
ТекущийМассивСтрокИД = Новый Массив;

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

// Не забываем обработать последний адрес после окончания цикла
Если НЕ ПустаяСтрока(ТекущийАдрес) Тогда
	ДобавитьАдресВГруппу(ГруппыРассылки, ТекущийАдрес, ТекущийМассивПодразделений, ТекущийМассивСтрокИД);
КонецЕсли;

Ниже представлена вспомогательная процедура ДобавитьАдресВГруппу, которая вычисляет строковый хэш и формирует группы рассылки:


Процедура ДобавитьАдресВГруппу(ГруппыРассылки, АдресПочты, МассивПодразделений, МассивСтрокИД)
	
	// Сортируем строковые идентификаторы, чтобы гарантировать совпадение хэшей
	МассивСтрокИД.Сортировать();
	ХэшНабора = СтрСоединить(МассивСтрокИД, "_");
	
	Группа = ГруппыРассылки.Получить(ХэшНабора);
	Если Группа = Неопределено Тогда
		Группа = Новый Структура;
		Группа.Вставить("Адреса", Новый Массив);
		Группа.Вставить("Подразделения", МассивПодразделений);
		ГруппыРассылки.Вставить(ХэшНабора, Группа);
	КонецЕсли;
	
	Группа.Адреса.Добавить(АдресПочты);
	
КонецПроцедуры

В результате работы данного алгоритма мы получаем готовую коллекцию групп. Каждый элемент коллекции содержит список уникальных подразделений (по которым нужно построить всего один отчет) и массив email-адресов получателей, которым данный отчет предназначается.

---

Альтернативный вариант: Математическое хэширование через сумму $2^N$

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

Суть метода заключается в присвоении каждому подразделению уникального порядкового номера $N$, а в качестве хэш-значения подразделения берется число $2^N$. В запросе выполняется группировка по адресу электронной почты, а хэшем набора подразделений становится сумма этих значений (битовая маска):


ВЫБРАТЬ
	Настройки.ЭлектроннаяПочта КАК ЭлектроннаяПочта,
	СУММА(Настройки.БитовыйКодПодразделения) КАК ХэшНабора
ИЗ
	ВТ_НормализованныеНастройки КАК Настройки
ГРУППИРОВАТЬ ПО
	Настройки.ЭлектроннаяПочта;

Преимущества метода:

Критические ограничения:

Вывод: этот метод красив с теоретической точки зрения, но опасен для крупных внедрений со сложной и динамично меняющейся структурой компании.

---

Шаг 3. Цикл генерации отчетов и отправка писем

Имея сформированное соответствие групп рассылки, мы можем запустить итоговый цикл генерации и отправки почты. Обратите внимание на важную деталь: при отправке письма группе получателей адреса необходимо помещать в поле СкрытаяКопия (Bcc), чтобы получатели не видели чужие email-адреса из соображений конфиденциальности.


Для Каждого ЭлементГруппы Из ГруппыРассылки Цикл
	
	ПараметрыГруппы = ЭлементГруппы.Значение;
	
	// 1. Формируем табличный документ по массиву подразделений группы
	ТабДок = СформироватьОтчетПоВыручке(ПараметрыГруппы.Подразделения);
	
	// 2. Подготавливаем почтовое сообщение
	Письмо = Новый ИнтернетПочтовоеСообщение;
	Письмо.Тема = "Выручка по подразделениям от " + Формат(ТекущаяДата(), "ДФ=dd.MM.yyyy");
	Письмо.Текст = "Добрый день! Во вложении отчет по выручке за отчетный период.";
	
	// Добавляем сформированный отчет во вложения
	ВременныйФайл = ПолучитьИмяВременногоФайла("xlsx");
	ТабДок.Записать(ВременныйФайл, ТипФайлаТабличногоДокумента.XLSX);
	Письмо.Вложения.Добавить(ВременныйФайл, "Выручка.xlsx");
	
	// Добавляем всех адресатов группы в СКРЫТУЮ копию
	Для Каждого АдресИз ГруппыПараметры.Адреса Цикл
		Адресат = Письмо.СкрытыеПолучатели.Добавить();
		Адресат.Адрес = АдресИз;
	КонецЦикла;
	
	// 3. Отправляем письмо через системную учетную запись
	// ИнтернетПочта.Отправить(Письмо);
	
	// Не забываем удалить временный файл
	УдалитьФайлы(ВременныйФайл);
	
КонецЦикла;

---

Шаг 4. Метод «нарезки» общего табличного документа (Альтернатива)

В сообщении 19 была предложена еще одна интересная архитектурная идея: сформировать один гигантский табличный документ по всем подразделениям сразу, а затем программно «нарезать» его на части для каждого адресата, копируя области строк.

Давайте проанализируем эту возможность по шагам:

  1. Формируем один сводный отчет по всей организации.
  2. Во время формирования запоминаем координаты строк начала и окончания вывода каждого подразделения в соответствие:
    
    // Пример фиксации координат областей
    ГраницыПодразделений.Вставить(ТекущееПодразделение, Новый Структура("Начало, Конец", ИсходныйТабДок.ВысотаТаблицы + 1, КонечнаяВысота));
    
  3. Для каждого уникального набора подразделений создаем новый пустой ТабличныйДокумент и методом ПолучитьОбласть() копируем нужные строки из общего отчета, собирая индивидуальный документ.

Преимущества метода:

Сложности реализации, о которых нужно помнить:

---

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

Мы проанализировали несколько отличных архитектурных подходов к оптимизации массовой рассылки в среде «1С:Предприятие».

Для большинства практических задач наиболее надежным и безопасным решением является Шаг 1 и Шаг 2 (группировка по строковому хэшу списка подразделений). Этот подход:

← На главную