В многопользовательских системах часто возникает задача эффективного распределения потока документов между сотрудниками. Типичный пример — работа менеджеров или сотрудников склада с общим списком заявок. Если два пользователя одновременно начнут обрабатывать один и тот же документ, это неизбежно приведет к конфликтам записи или дублированию бизнес-процессов. В этой статье мы подробно разберем, как реализовать механизм «захвата» документа, чтобы при выборе его одним пользователем он автоматически исчезал из списка у всех остальных.
Представим ситуацию: у нас есть обработка, на первом шаге которой отображается список документов. Как только пользователь №1 выбирает документ для работы, этот объект должен стать невидимым для пользователя №2, открывшего ту же обработку. При этом мы должны учитывать риск некорректного завершения сеанса (отключение света, зависание компьютера), чтобы документ не остался «заблокированным» навсегда.
Проанализируем возможные варианты реализации:
ЗаблокироватьДанныеДляРедактирования(). Метод хорош тем, что платформа сама снимает блокировку при закрытии формы или падении сеанса. Однако он предназначен скорее для редактирования, а не для логического «скрытия» из списков.ДокументВыбран непосредственно в объекте. Это не элегантно, так как требует изменения структуры метаданных и может привести к лишним блокировкам при записи самого документа.Для начала нам необходимо создать независимый Регистр Сведений (назовем его ЗанятыеДокументы). Рассмотрим оптимальную структуру этого регистра:
Объект (Тип: ДокументСсылка) — основной идентификатор захваченного документа.Пользователь (Тип: СправочникСсылка.Пользователи) — кто захватил документ.ДатаЗахвата (Тип: Дата) — время начала работы.ИдентификаторФормы (Тип: УникальныйИдентификатор) — это позволит нам различать ситуации, когда один и тот же пользователь открыл несколько окон обработки.Разберем, как происходит процесс захвата. Когда пользователь выбирает строку в динамическом списке на форме обработки, мы должны выполнить запись в наш регистр. Важно помнить о Race Condition (состоянии гонки): два пользователя могут нажать на одну строку почти одновременно. Поэтому перед открытием следующего шага мы обязательно проверяем, не успел ли кто-то другой захватить этот документ долей секунды ранее.
Рассмотрим пример кода для записи в регистр:
&НаСервере
Функция ЗахватитьДокумент(ВыбранныйДокумент, УникальныйИдентификаторФормы)
МенеджерЗаписи = РегистрыСведений.ЗанятыеДокументы.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Объект = ВыбранныйДокумент;
МенеджерЗаписи.Прочитать();
Если МенеджерЗаписи.Выбран() Тогда
Возврат Ложь; // Документ уже занят кем-то другим
КонецЕсли;
МенеджерЗаписи.Объект = ВыбранныйДокумент;
МенеджерЗаписи.Пользователь = ПараметрыСеанса.ТекущийПользователь;
МенеджерЗаписи.ДатаЗахвата = ТекущаяДатаСеанса();
МенеджерЗаписи.ИдентификаторФормы = УникальныйИдентификаторФормы;
Попытка
МенеджерЗаписи.Записать();
Возврат Истина;
Исключение
Возврат Ложь;
КонецПопытки;
КонецФункции
Теперь выясним, как скрыть эти документы из общего списка. В динамическом списке на форме обработки нам нужно изменить текст запроса. Мы добавим ЛЕВОЕ СОЕДИНЕНИЕ с нашим новым регистром и установим условие, что запись в регистре должна отсутствовать.
Проанализируем пример запроса для динамического списка:
ВЫБРАТЬ
Док.Ссылка КАК Ссылка,
Док.Номер КАК Номер,
Док.Дата КАК Дата
ИЗ
Документ.ЗаказПокупателя КАК Док
ЛЕВОЕ СОЕДИНЕНИЕ
РегистрСведений.ЗанятыеДокументы КАК Занятые
ПО
Док.Ссылка = Занятые.Объект
ГДЕ
Занятые.Объект ЕСТЬ NULL
И Док.Проведен = ИСТИНА
Важный нюанс: Чтобы список обновлялся оперативно, можно использовать оповещения формы или периодический вызов метода Элементы.Список.Обновить() через обработчик ожидания. Однако помните, что слишком частое обновление может нагружать сервер.
Чтобы система работала корректно, мы должны очищать записи в регистре, когда пользователь заканчивает работу. Рассмотрим основные сценарии:
ПриЗакрытии или ПередЗакрытием необходимо вызвать серверную процедуру, которая удалит запись из регистра ЗанятыеДокументы по фильтру ИдентификаторФормы.ПодключитьОбработчикОжидания, который раз в минуту обновляет ДатаЗахвата в регистре. Это подтверждает системе, что пользователь всё еще активно работает с документом.ДатаЗахвата отличается от текущего времени более чем на 5–10 минут (и не обновлялась «пульсацией»), значит сеанс завершился некорректно, и запись можно удалять.Рассмотрим пример кода для регламентного задания очистки:
Процедура ОчиститьПодвисшиеЗахваты() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Занятые.Объект КАК Объект
|ИЗ
| РегистрСведений.ЗанятыеДокументы КАК Занятые
|ГДЕ
| Занятые.ДатаЗахвата < &КритическаяДата";
// Считаем захват подвисшим, если он старше 10 минут
Запрос.УстановитьПараметр("КритическаяДата", ТекущаяДатаСеанса() - 600);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Набор = РегистрыСведений.ЗанятыеДокументы.СоздатьНаборЗаписей();
Набор.Отбор.Объект.Установить(Выборка.Объект);
Набор.Записать();
КонецЦикла;
КонецПроцедуры
Если в вашей конфигурации внедрена Система взаимодействия, мы можем сделать механизм еще более современным. При захвате документа пользователем №1, сервер может отправить фоновое сообщение всем остальным открытым формам. Получив такое сообщение, форма пользователя №2 может автоматически обновить динамический список или просто пометить строку серым цветом, не дожидаясь действий пользователя. Это обеспечивает максимальную интерактивность интерфейса.
Мы рассмотрели надежный способ реализации эксклюзивной работы с документами. Использование независимого регистра сведений в сочетании с грамотной очисткой через регламентные задания позволяет избежать «мусорных» данных и конфликтов доступа. Главное преимущество такого подхода — его прозрачность для разработчика и легкость в отладке. Помните о важности проверки статуса захвата непосредственно перед началом транзакционных действий, чтобы исключить риск одновременного нажатия кнопок разными пользователями.