ZKTeco и 1С связывают через SDK zkemkeeper — COM-библиотеку ZKTeco для разработчиков. Через неё 1С читает с биометрического терминала (отпечаток, лицо, карта) журнал отметок, пользователей и время, и покупной модуль не нужен. Ниже — регистрация SDK из практики, обход 32-битности на 64-битном сервере 1С, общий модуль чтения терминала, свой регистр сведений, загрузка по расписанию и запрос «опоздания / отработано».
Новый COMОбъект("zkemkeeper.ZKEM") падает с «Класс не зарегистрирован».zkemkeeper.dll — 32-битная внутрипроцессная COM-библиотека: у сборок 6.2.5.5 и 6.3.1.38 из комплекта автора заголовок PE — x86. Загрузить её может только 32-битный процесс, поэтому схема зависит от того, кто создаёт объект.
| Окружение | Где работает SDK | Расписание |
|---|---|---|
| Файловая база, 32-битный клиент | Везде: серверный код файловой базы выполняется в процессе клиента | Сеанс 32-битного клиента |
| 32-битный сервер 1С | На сервере | Регламентное задание |
| 64-битный сервер 1С | Только в клиентском коде 32-битного клиента | Клиент, запущенный Планировщиком Windows |
| 64-битный сервер + DllSurrogate | На сервере: объект живёт в 32-битном dllhost.exe | Регламентное задание |
| Прослойка | 32-битная служба (C#, Python) отдаёт отметки файлом или в HTTP-сервис | Своё у службы |
32-битный тонкий клиент запускают из C:\Program Files (x86)\1cv8\<версия>\bin\1cv8c.exe или в окне запуска задают в свойствах базы разрядность «32 (x86)» (параметр AppArch в ibases.v8i). В веб-клиенте COMОбъект по справке поддерживается только в Internet Explorer — этот вариант не рассматриваем. Прослойку стоит писать, если реестр сервера трогать нельзя, а держать открытый клиент негде.
Из практики автора: SDK лежит в отдельной папке и регистрируется 32-битным regsvr32 — на 64-битной Windows это копия из SysWOW64, а не из System32. Сначала проверьте, не зарегистрировала ли библиотеку другая программа ZKTeco: новая регистрация перепишет путь к DLL.
:: Регистрация zkemkeeper на 64-битной Windows. Командная строка от имени администратора.
:: 1. Нет ли уже регистрации от другой программы ZKTeco: CLSID и путь к её DLL
reg query "HKCR\zkemkeeper.ZKEM\CLSID"
reg query "HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\{00853A19-BD51-419B-9269-2DABE57EB61F}\InprocServer32"
:: 2. Если есть - копия ветки: новая регистрация перепишет путь, вернуть можно через reg import
reg export "HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\{00853A19-BD51-419B-9269-2DABE57EB61F}" C:\zkemkeeper\clsid-backup.reg
:: 3. Из практики: 32-битный regsvr32, в C:\zkemkeeper - весь комплект DLL из SDK
C:\Windows\SysWOW64\regsvr32 "C:\zkemkeeper\zkemkeeper.dll"
:: Отмена регистрации - тем же regsvr32:
:: C:\Windows\SysWOW64\regsvr32 /u "C:\zkemkeeper\zkemkeeper.dll"
*.dll в %windir%\SysWOW64 и регистрируют оттуда, второй — с ключом /s, без окон: о сбое вы не узнаете. copy в bat-файле перезаписывает одноимённые файлы без вопроса, и DLL другой версии от программы ZKTeco будут заменены. Своя папка безопаснее. Но если объект создаётся, а Connect_Net при живой сети возвращает Ложь, проверьте, находит ли процесс остальные DLL комплекта.Чтобы регламентное задание на 64-битном сервере работало с SDK, zkemkeeper запускают вне процесса, в системном суррогате dllhost.exe. По документации Microsoft для этого у AppID класса задают значение DllSurrogate; пустая строка — системный суррогат. Ветка CLSID 32-битной регистрации лежит в WOW6432Node, ветка AppID общая для 32 и 64 бит. CLSID zkemkeeper.ZKEM в обеих сборках у автора — {00853A19-BD51-419B-9269-2DABE57EB61F}, у своей сверьте первой командой предыдущего листинга.
:: DllSurrogate для 64-битного сервера 1С. Командная строка от имени администратора на сервере.
set CLSID={00853A19-BD51-419B-9269-2DABE57EB61F}
:: 1. Копия ветки класса до правки
reg export "HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\%CLSID%" C:\zkemkeeper\clsid-before-surrogate.reg
:: 2. Есть ли у класса AppID: у zkemkeeper он может быть уже прописан
reg query "HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\%CLSID%" /v AppID
:: 3. AppID есть - подставьте его значение из шага 2:
set APPID={GUID-из-шага-2}
:: AppID нет - назначьте классу его же GUID (вместо строки выше):
:: reg add "HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\%CLSID%" /v AppID /t REG_SZ /d %CLSID%
:: set APPID=%CLSID%
:: 4. Пустой DllSurrogate - системный суррогат dllhost.exe. Ветка AppID общая для 32 и 64 бит
reg add "HKLM\SOFTWARE\Classes\AppID\%APPID%" /v DllSurrogate /t REG_SZ /d ""
:: Откат: reg delete "HKLM\SOFTWARE\Classes\AppID\%APPID%" /v DllSurrogate /f
:: и, если AppID назначали сами: reg delete "HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\%CLSID%" /v AppID /f
Запускать COM-серверы по умолчанию могут только Administrators, SYSTEM и INTERACTIVE, а служба 1С обычно работает от своей учётной записи, например usr1cv8, — без разрешения будет «Отказано в доступе» (0x80070005). В dcomcnfg откройте «Службы компонентов → Компьютеры → Мой компьютер → Настройка DCOM», найдите приложение (zkemkeeper или GUID из AppID) и в «Свойства → Безопасность → Разрешения на запуск и активацию → Настроить → Изменить…» дайте учётной записи службы «Локальный запуск» и «Локальная активация». Вызовы методов теперь идут между процессами и работают медленнее.
ОтметкиСКУД — периодичность «В пределах секунды», режим записи «Независимый». Ключ записи — время, терминал и сотрудник, поэтому повторная загрузка дублей не даёт.
| Поле | Вид | Тип | Что хранит |
|---|---|---|---|
| Период | — | Дата и время | Время отметки по часам терминала |
| Терминал | Измерение | Строка (30) | Серийный номер терминала |
| КодСотрудника | Измерение | Строка (24) | ID на терминале, по руководству SDK — до 24 цифр |
| СпособПроверки, Статус | Ресурсы | Число (5, 0) | Способ проверки (0 — пароль, 1 — отпечаток, 2 — карта) и статус, выбранный на терминале |
| РабочийКод | Ресурс | Число (10, 0) | Рабочий код (work code) |
| Загружено | Реквизит | Дата и время | Когда отметка попала в 1С |
СотрудникиСКУД — непериодический, независимый: измерение КодСотрудника (Строка 24), ресурсы ИмяНаТерминале (Строка 50) и Сотрудник — СправочникСсылка.Сотрудники в КА 2.5 или СправочникСсылка.ФизическиеЛица в УТ 11. Регистры можно добавить расширением, не снимая типовую с поддержки. Про виды регистров — в статье о регистре сведений.
Модуль работает там, где окажется COM-объект. ЗагрузитьОтметки проходит весь цикл: подключение, часы, пользователи, отметки, запись в 1С и по флажку очистка журнала.
// Общий модуль ТерминалZKTeco. Флажки «Клиент (управляемое приложение)» и «Сервер»:
// код работает там, где создан COM-объект, - в 32-битном клиенте или на сервере
// (32-битный сервер 1С, DllSurrogate). Методы SDK - по ZKTeco Standalone SDK Development Manual.
// Полный цикл с одним терминалом: часы, пользователи, отметки, по желанию очистка журнала.
// В базу пишет серверный модуль ОтметкиСКУДСервер. Возвращает итог строкой.
Функция ЗагрузитьОтметки(Знач АдресIP, Знач Очистить = Ложь) Экспорт
Терминал = Подключиться(АдресIP);
Попытка
Серийный = СерийныйНомер(Терминал);
Расхождение = СинхронизироватьВремя(Терминал, ОтметкиСКУДСервер.ВремяСервера());
ОтметкиСКУДСервер.ЗаписатьСотрудников(ПрочитатьСотрудников(Терминал));
Если Очистить Тогда
// Выключенный терминал не примет отметку, которую очистка удалила бы незагруженной
Если Не Терминал.EnableDevice(НомерТерминала(), Ложь) Тогда
ВызватьОшибкуSDK(Терминал, НСтр("ru = 'Выключение терминала'"));
КонецЕсли;
Отметки = ПрочитатьОтметки(Терминал);
Иначе
Отметки = ПрочитатьОтметки(Терминал, ОтметкиСКУДСервер.ЧитатьНачинаяС(Серийный));
КонецЕсли;
Новых = ОтметкиСКУДСервер.ЗаписатьОтметки(Отметки, Серийный);
Если Очистить Тогда
ОчиститьЖурнал(Терминал); // всё прочитанное уже в регистре
Терминал.EnableDevice(НомерТерминала(), Истина);
КонецЕсли;
Исключение
Отключиться(Терминал, Очистить);
ВызватьИсключение;
КонецПопытки;
Отключиться(Терминал);
Возврат СтрШаблон(НСтр("ru = '%1 (%2): прочитано %3, новых %4, часы расходились на %5 с'"),
АдресIP, Серийный, Отметки.Количество(), Новых, Расхождение);
КонецФункции
// Подключается к терминалу по сети. Возвращает COM-объект SDK или вызывает исключение.
Функция Подключиться(Знач АдресIP, Знач Порт = 4370) Экспорт
Попытка
Терминал = Новый COMОбъект("zkemkeeper.ZKEM");
Исключение
// В 64-битном процессе без DllSurrogate здесь будет «Класс не зарегистрирован»
ВызватьИсключение СтрШаблон(НСтр("ru = 'Не создан объект zkemkeeper.ZKEM: %1'"),
КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
Если Не Терминал.Connect_Net(АдресIP, Порт) Тогда
ВызватьОшибкуSDK(Терминал, СтрШаблон(НСтр("ru = 'Подключение к %1:%2'"),
АдресIP, Формат(Порт, "ЧГ=0")), Истина);
КонецЕсли;
Возврат Терминал;
КонецФункции
// Закрывает соединение; ВключитьОбратно - если терминал выключали для очистки.
// Ошибки не выпускает: вызывается и из обработчика исключения, где важна исходная ошибка.
Процедура Отключиться(Терминал, Знач ВключитьОбратно = Ложь) Экспорт
Попытка
Если ВключитьОбратно Тогда
Терминал.EnableDevice(НомерТерминала(), Истина);
КонецЕсли;
Терминал.Disconnect();
Исключение
// связь уже потеряна
КонецПопытки;
КонецПроцедуры
// Серийный номер - постоянный ключ терминала в регистре: IP-адрес может смениться.
Функция СерийныйНомер(Терминал) Экспорт
Номер = "";
Если Не Терминал.GetSerialNumber(НомерТерминала(), Номер) Тогда
ВызватьОшибкуSDK(Терминал, НСтр("ru = 'Чтение серийного номера'"));
КонецЕсли;
Возврат СокрЛП(Номер);
КонецФункции
// Отметки не раньше НачинаяС: массив структур, его можно передать с клиента на сервер.
Функция ПрочитатьОтметки(Терминал, Знач НачинаяС = '00010101') Экспорт
Отметки = Новый Массив;
// Журнал целиком копируется в буфер SDK; Ложь с кодом 0 - журнал пуст
Если Не ДанныеПрочитаны(Терминал, Терминал.ReadGeneralLogData(НомерТерминала()),
НСтр("ru = 'Чтение журнала отметок'")) Тогда
Возврат Отметки;
КонецЕсли;
// Выходные параметры: переменные заранее получают значения нужного типа
КодСотрудника = "";
СпособПроверки = 0;
Статус = 0;
РабочийКод = 0;
ГодОтметки = 0; МесяцОтметки = 0; ДеньОтметки = 0;
ЧасОтметки = 0; МинутаОтметки = 0; СекундаОтметки = 0;
// Каждый вызов отдаёт следующую запись буфера, после последней - Ложь
Пока Терминал.SSR_GetGeneralLogData(НомерТерминала(), КодСотрудника, СпособПроверки, Статус,
ГодОтметки, МесяцОтметки, ДеньОтметки, ЧасОтметки, МинутаОтметки, СекундаОтметки, РабочийКод) Цикл
Если ГодОтметки < 2000 Тогда
Продолжить; // защита от записи с пустой датой: Дата() на ней упадёт
КонецЕсли;
Период = Дата(ГодОтметки, МесяцОтметки, ДеньОтметки, ЧасОтметки, МинутаОтметки, СекундаОтметки);
Если Период >= НачинаяС Тогда
Отметки.Добавить(Новый Структура("Период, КодСотрудника, СпособПроверки, Статус, РабочийКод",
Период, СокрЛП(КодСотрудника), СпособПроверки, Статус, РабочийКод));
КонецЕсли;
КонецЦикла;
Возврат Отметки;
КонецФункции
// Пользователи терминала: массив структур (КодСотрудника, Имя, Права, Включен).
// Шаблоны отпечатков и лиц метод не читает; пароль с терминала в 1С не переносим.
Функция ПрочитатьСотрудников(Терминал) Экспорт
Сотрудники = Новый Массив;
Если Не ДанныеПрочитаны(Терминал, Терминал.ReadAllUserID(НомерТерминала()),
НСтр("ru = 'Чтение пользователей терминала'")) Тогда
Возврат Сотрудники;
КонецЕсли;
КодСотрудника = "";
Имя = "";
ПарольНаТерминале = "";
Права = 0; // 0 - пользователь, 1 - регистратор, 2 - администратор, 3 - суперадминистратор
Включен = Ложь;
Пока Терминал.SSR_GetAllUserInfo(НомерТерминала(), КодСотрудника, Имя, ПарольНаТерминале, Права, Включен) Цикл
Сотрудники.Добавить(Новый Структура("КодСотрудника, Имя, Права, Включен",
СокрЛП(КодСотрудника), СокрЛП(Имя), Права, Включен));
КонецЦикла;
Возврат Сотрудники;
КонецФункции
// Ставит на терминале время Эталон, если часы ушли больше чем на Допуск секунд.
// Возвращает расхождение до правки в секундах: плюс - терминал спешил.
Функция СинхронизироватьВремя(Терминал, Знач Эталон, Знач Допуск = 30) Экспорт
Расхождение = ВремяТерминала(Терминал) - Эталон;
Если ?(Расхождение < 0, -Расхождение, Расхождение) > Допуск Тогда
Если Не Терминал.SetDeviceTime2(НомерТерминала(), Год(Эталон), Месяц(Эталон), День(Эталон),
Час(Эталон), Минута(Эталон), Секунда(Эталон)) Тогда
ВызватьОшибкуSDK(Терминал, НСтр("ru = 'Установка времени терминала'"));
КонецЕсли;
КонецЕсли;
Возврат Расхождение;
КонецФункции
// Часы терминала как дата 1С.
Функция ВремяТерминала(Терминал) Экспорт
ГодТерминала = 0; МесяцТерминала = 0; ДеньТерминала = 0;
ЧасТерминала = 0; МинутаТерминала = 0; СекундаТерминала = 0;
Если Не Терминал.GetDeviceTime(НомерТерминала(), ГодТерминала, МесяцТерминала, ДеньТерминала,
ЧасТерминала, МинутаТерминала, СекундаТерминала) Тогда
ВызватьОшибкуSDK(Терминал, НСтр("ru = 'Чтение времени терминала'"));
КонецЕсли;
Возврат Дата(ГодТерминала, МесяцТерминала, ДеньТерминала, ЧасТерминала, МинутаТерминала, СекундаТерминала);
КонецФункции
// Удаляет все отметки на терминале. Только на выключенном терминале
// и только после записи прочитанного в 1С - см. ЗагрузитьОтметки.
Процедура ОчиститьЖурнал(Терминал)
Если Не Терминал.ClearGLog(НомерТерминала()) Тогда
ВызватьОшибкуSDK(Терминал, НСтр("ru = 'Очистка журнала отметок'"));
КонецЕсли;
Терминал.RefreshData(НомерТерминала());
КонецПроцедуры
// Номер терминала (Device ID) из его настроек связи.
Функция НомерТерминала()
Возврат 1;
КонецФункции
// Истина - данные в буфере SDK; Ложь - данных нет (код 0); иначе исключение.
Функция ДанныеПрочитаны(Терминал, Знач Успех, Знач Действие)
Если Успех Тогда
Возврат Истина;
КонецЕсли;
Код = КодОшибкиSDK(Терминал);
Если Код <> 0 Тогда
ВызватьОшибкуSDK(Терминал, Действие, Ложь, Код);
КонецЕсли;
Возврат Ложь;
КонецФункции
// Код последней ошибки: GetLastError возвращает его в выходном параметре.
Функция КодОшибкиSDK(Терминал)
Код = 0;
Терминал.GetLastError(Код);
Возврат Код;
КонецФункции
Процедура ВызватьОшибкуSDK(Терминал, Знач Действие, Знач ПриПодключении = Ложь, Знач Код = Неопределено)
Если Код = Неопределено Тогда
Код = КодОшибкиSDK(Терминал);
КонецЕсли;
ВызватьИсключение СтрШаблон(НСтр("ru = '%1: ошибка SDK %2 (%3)'"),
Действие, Формат(Код, "ЧН=0; ЧГ=0"), ОписаниеОшибкиSDK(Код, ПриПодключении));
КонецПроцедуры
// Расшифровка кодов GetLastError по руководству SDK. При подключении часть кодов значит другое.
Функция ОписаниеОшибкиSDK(Знач Код, Знач ПриПодключении)
Описания = Новый Соответствие;
Если ПриПодключении Тогда
Описания.Вставить(-1, НСтр("ru = 'не удалось вызвать интерфейс'"));
Описания.Вставить(-2, НСтр("ru = 'ошибка инициализации'"));
Описания.Вставить(-6, НСтр("ru = 'неверный пароль'"));
Описания.Вставить(-7, НСтр("ru = 'ошибка ответа'"));
Описания.Вставить(-8, НСтр("ru = 'истекло время ожидания ответа'"));
Описания.Вставить(-307, НСтр("ru = 'истекло время подключения'"));
Описания.Вставить(-12001, НСтр("ru = 'истекло время создания сокета'"));
Иначе
Описания.Вставить(-201, НСтр("ru = 'терминал занят'"));
Описания.Вставить(-100, НСтр("ru = 'не поддерживается или данных нет'"));
Описания.Вставить(-10, НСтр("ru = 'неверная длина данных'"));
Описания.Вставить(-4, НСтр("ru = 'не хватает места'"));
Описания.Вставить(-2, НСтр("ru = 'ошибка чтения или записи файла'"));
Описания.Вставить(-1, НСтр("ru = 'SDK не инициализирован, нужно переподключиться'"));
Описания.Вставить(0, НСтр("ru = 'данные не найдены или повторяются'"));
Описания.Вставить(4, НСтр("ru = 'неверный параметр'"));
КонецЕсли;
Описание = Описания.Получить(Код);
Возврат ?(Описание = Неопределено, НСтр("ru = 'см. GetLastError в руководстве SDK'"), Описание);
КонецФункции
"zkemkeeper.ZKEM" — ProgID без номера версии. В DLL прописаны zkemkeeper.ZKEM.1 и zkemkeeper.ZKEM.Connect_Net(АдресIP, Порт) — порт по умолчанию 4370. При Ложь код берут из GetLastError; при подключении коды значат другое, поэтому в ОписаниеОшибкиSDK два набора.BSTR*, числом для LONG*, булевым для VARIANT_BOOL*.ReadGeneralLogData копирует журнал в буфер SDK, SSR_GetGeneralLogData отдаёт по записи, пока не вернёт Ложь. Ложь с кодом 0 у ReadGeneralLogData — «данных нет», а не сбой.SSR_-методы по руководству SDK — для терминалов с цветным экраном. У старых чёрно-белых моделей — GetGeneralLogData: код сотрудника там число, секунд нет.SetDeviceTime2 ставит время сервера 1С, если расхождение больше допуска. SetDeviceTime поставил бы время компьютера, где работает SDK.EnableDevice(..., Ложь) выключает отпечаток, карту и клавиатуру: пока журнал читается, пишется в 1С и очищается, новая отметка не появится и не пропадёт. При ошибке Отключиться включает терминал обратно.Серверный модуль пишет только новые отметки: прочитанное сравнивается с регистром запросом, а управляемая блокировка по терминалу не даст ручной загрузке и регламентному заданию записать одно и то же дважды. Про наборы записей и менеджер записи — в статье о записи в регистр сведений.
// Общий модуль ОтметкиСКУДСервер. Флажки «Сервер» и «Вызов сервера».
// Пишет в регистр ОтметкиСКУД отметки, которых там ещё нет. Возвращает количество новых.
Функция ЗаписатьОтметки(Знач Отметки, Знач Терминал) Экспорт
Если Отметки.Количество() = 0 Тогда
Возврат 0;
КонецЕсли;
// Колонкам нужны типы: таблица уходит в запрос параметром
Таблица = Новый ТаблицаЗначений;
Таблица.Колонки.Добавить("Период", Новый ОписаниеТипов("Дата", , , Новый КвалификаторыДаты(ЧастиДаты.ДатаВремя)));
Таблица.Колонки.Добавить("КодСотрудника", Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(24)));
Таблица.Колонки.Добавить("СпособПроверки", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(5, 0)));
Таблица.Колонки.Добавить("Статус", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(5, 0)));
Таблица.Колонки.Добавить("РабочийКод", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(10, 0)));
Для Каждого Отметка Из Отметки Цикл
ЗаполнитьЗначенияСвойств(Таблица.Добавить(), Отметка);
КонецЦикла;
// Только отметки, которых нет в регистре; одна строка на ключ записи
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Т.Период КАК Период,
| Т.КодСотрудника КАК КодСотрудника,
| Т.СпособПроверки КАК СпособПроверки,
| Т.Статус КАК Статус,
| Т.РабочийКод КАК РабочийКод
|ПОМЕСТИТЬ ВТПрочитанные
|ИЗ
| &Таблица КАК Т
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Прочитанные.Период КАК Период,
| Прочитанные.КодСотрудника КАК КодСотрудника,
| МАКСИМУМ(Прочитанные.СпособПроверки) КАК СпособПроверки,
| МАКСИМУМ(Прочитанные.Статус) КАК Статус,
| МАКСИМУМ(Прочитанные.РабочийКод) КАК РабочийКод
|ИЗ
| ВТПрочитанные КАК Прочитанные
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ОтметкиСКУД КАК Отметки
| ПО (Отметки.Терминал = &Терминал)
| И Прочитанные.Период = Отметки.Период
| И Прочитанные.КодСотрудника = Отметки.КодСотрудника
|ГДЕ
| Отметки.Период ЕСТЬ NULL
|
|СГРУППИРОВАТЬ ПО
| Прочитанные.Период,
| Прочитанные.КодСотрудника";
Запрос.УстановитьПараметр("Таблица", Таблица);
Запрос.УстановитьПараметр("Терминал", Терминал);
// Ручная загрузка и регламентное задание не пишут один терминал одновременно
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ОтметкиСКУД");
ЭлементБлокировки.УстановитьЗначение("Терминал", Терминал);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
НачатьТранзакцию();
Попытка
Блокировка.Заблокировать();
Новые = Запрос.Выполнить().Выгрузить();
Набор = РегистрыСведений.ОтметкиСКУД.СоздатьНаборЗаписей();
Загружено = ТекущаяДатаСеанса();
Для Каждого СтрокаНовых Из Новые Цикл
Запись = Набор.Добавить();
ЗаполнитьЗначенияСвойств(Запись, СтрокаНовых);
Запись.Терминал = Терминал;
Запись.Загружено = Загружено;
КонецЦикла;
// Ложь - только добавить: с Истиной набор без отбора заменил бы весь регистр
Набор.Записать(Ложь);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Возврат Новые.Количество();
КонецФункции
// С какого момента читать журнал: последняя загруженная отметка терминала минус сутки.
// Запас - на случай, если часы терминала переводили назад; повторы отсеет ЗаписатьОтметки.
Функция ЧитатьНачинаяС(Знач Терминал) Экспорт
Запрос = Новый Запрос(
"ВЫБРАТЬ
| МАКСИМУМ(Отметки.Период) КАК Период
|ИЗ
| РегистрСведений.ОтметкиСКУД КАК Отметки
|ГДЕ
| Отметки.Терминал = &Терминал");
Запрос.УстановитьПараметр("Терминал", Терминал);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() И ЗначениеЗаполнено(Выборка.Период) Тогда
Возврат Выборка.Период - 86400;
КонецЕсли;
Возврат '00010101';
КонецФункции
// Имена с терминала - в регистр СотрудникиСКУД. Сотрудника 1С в нём
// сопоставляют вручную, загрузка это поле не трогает.
Процедура ЗаписатьСотрудников(Знач Сотрудники) Экспорт
Для Каждого Сотрудник Из Сотрудники Цикл
Менеджер = РегистрыСведений.СотрудникиСКУД.СоздатьМенеджерЗаписи();
Менеджер.КодСотрудника = Сотрудник.КодСотрудника;
Менеджер.Прочитать();
Если Менеджер.Выбран() И Менеджер.ИмяНаТерминале = Сотрудник.Имя Тогда
Продолжить; // имя не менялось
КонецЕсли;
Менеджер.КодСотрудника = Сотрудник.КодСотрудника; // ключ ещё раз: записи могло не быть
Менеджер.ИмяНаТерминале = Сотрудник.Имя;
Менеджер.Записать();
КонецЦикла;
КонецПроцедуры
// Эталон для часов терминала - время сервера 1С.
Функция ВремяСервера() Экспорт
Возврат ТекущаяДатаСеанса();
КонецФункции
// Обработчик регламентного задания «Загрузка отметок СКУД»: раз в 10-15 минут.
// Работает, если zkemkeeper создаётся на сервере: 32-битный сервер 1С или DllSurrogate.
Процедура ЗагрузкаОтметокПоРасписанию() Экспорт
Для Каждого АдресIP Из АдресаТерминалов() Цикл
Попытка
Итог = ТерминалZKTeco.ЗагрузитьОтметки(АдресIP);
ЗаписьЖурналаРегистрации(ИмяСобытия(), УровеньЖурналаРегистрации.Информация, , , Итог);
Исключение
// Недоступный терминал не мешает остальным
ЗаписьЖурналаРегистрации(ИмяСобытия(), УровеньЖурналаРегистрации.Ошибка, , ,
АдресIP + ": " + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецЦикла;
КонецПроцедуры
// Адреса терминалов. Для примера - строкой; в рабочей базе держите их в константе или справочнике.
Функция АдресаТерминалов()
Возврат СтрРазделить("192.168.1.10,192.168.1.11", ",", Ложь);
КонецФункции
Функция ИмяСобытия()
Возврат НСтр("ru = 'СКУД.Загрузка отметок'");
КонецФункции
Набор.Записать(Ложь) — только добавление. У набора нет отбора, и с Истина он заменил бы весь регистр.СГРУППИРОВАТЬ ПО ключу записи: две отметки сотрудника в одну секунду не уронят запись набора.ЧитатьНачинаяС отдаёт последнюю загруженную отметку минус сутки: запас на случай, если часы терминала переводили назад.Регламентное задание с методом ОтметкиСКУДСервер.ЗагрузкаОтметокПоРасписанию добавьте в конфигурацию или расширение, расписание — в статье о регламентном задании. Оно работает, где SDK создаётся на сервере. На 64-битном сервере без DllSurrogate Планировщик Windows запускает 32-битный клиент с ключом /Execute (разбор схемы): форма обработки вызывает ТерминалZKTeco.ЗагрузитьОтметки и закрывает сеанс.
// Модуль формы обработки «Загрузка отметок ZKTeco». Открывать в 32-битном тонком клиенте:
// SDK создаётся на клиенте, сервер 1С при этом может быть 64-битным.
// Реквизиты формы: АдресIP (Строка), ОчиститьЖурнал (Булево); команда Загрузить.
&НаКлиенте
Процедура Загрузить(Команда)
Если ПустаяСтрока(АдресIP) Тогда
ПоказатьПредупреждение(, НСтр("ru = 'Укажите IP-адрес терминала'"));
Возврат;
КонецЕсли;
Если ОчиститьЖурнал Тогда
ПоказатьВопрос(Новый ОписаниеОповещения("ЗагрузитьПослеВопроса", ЭтотОбъект),
НСтр("ru = 'После загрузки журнал на терминале будет очищен. Продолжить?'"),
РежимДиалогаВопрос.ДаНет);
Иначе
ВыполнитьЗагрузку();
КонецЕсли;
КонецПроцедуры
&НаКлиенте
Процедура ЗагрузитьПослеВопроса(Ответ, ДополнительныеПараметры) Экспорт
Если Ответ = КодВозвратаДиалога.Да Тогда
ВыполнитьЗагрузку();
КонецЕсли;
КонецПроцедуры
&НаКлиенте
Процедура ВыполнитьЗагрузку()
Попытка
Итог = ТерминалZKTeco.ЗагрузитьОтметки(СокрЛП(АдресIP), ОчиститьЖурнал);
Исключение
ПоказатьПредупреждение(, КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
Возврат;
КонецПопытки;
ПоказатьОповещениеПользователя(НСтр("ru = 'Отметки загружены'"), , Итог);
КонецПроцедуры
Первая отметка за день — приход, последняя — уход. Статус с терминала отчёт не учитывает: его забывают выбирать. Код сотрудника делайте одинаковым на всех терминалах, тогда отметки с проходной и склада сойдутся. Запрос проверьте в консоли запросов, затем перенесите в схему компоновки.
// Консоль запросов или отчёт на СКД. Параметры: НачалоПериода, КонецПериода (дата и время),
// НачалоСмены - начало рабочего дня в секундах от полуночи (9:00 = 32400).
ВЫБРАТЬ
Отметки.КодСотрудника КАК КодСотрудника,
НАЧАЛОПЕРИОДА(Отметки.Период, ДЕНЬ) КАК День,
МИНИМУМ(Отметки.Период) КАК Приход,
МАКСИМУМ(Отметки.Период) КАК Уход,
КОЛИЧЕСТВО(*) КАК ЧислоОтметок
ПОМЕСТИТЬ ВТДни
ИЗ
РегистрСведений.ОтметкиСКУД КАК Отметки
ГДЕ
Отметки.Период МЕЖДУ &НачалоПериода И &КонецПериода
СГРУППИРОВАТЬ ПО
Отметки.КодСотрудника,
НАЧАЛОПЕРИОДА(Отметки.Период, ДЕНЬ)
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Дни.День КАК День,
Сотрудники.Сотрудник КАК Сотрудник,
ЕСТЬNULL(Сотрудники.ИмяНаТерминале, "") КАК ИмяНаТерминале,
Дни.КодСотрудника КАК КодСотрудника,
Дни.Приход КАК Приход,
Дни.Уход КАК Уход,
ВЫБОР
КОГДА Дни.Приход > ДОБАВИТЬКДАТЕ(Дни.День, СЕКУНДА, &НачалоСмены)
ТОГДА РАЗНОСТЬДАТ(ДОБАВИТЬКДАТЕ(Дни.День, СЕКУНДА, &НачалоСмены), Дни.Приход, МИНУТА)
ИНАЧЕ 0
КОНЕЦ КАК ОпозданиеМинут,
ВЫРАЗИТЬ(РАЗНОСТЬДАТ(Дни.Приход, Дни.Уход, МИНУТА) / 60 КАК ЧИСЛО(6, 2)) КАК ОтработаноЧасов,
ВЫБОР
КОГДА Дни.ЧислоОтметок = 1
ТОГДА ИСТИНА
ИНАЧЕ ЛОЖЬ
КОНЕЦ КАК ОднаОтметка
ИЗ
ВТДни КАК Дни
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СотрудникиСКУД КАК Сотрудники
ПО Дни.КодСотрудника = Сотрудники.КодСотрудника
УПОРЯДОЧИТЬ ПО
День,
КодСотрудника
Ограничения: обед не вычитается, начало смены одно на всех, ночная смена через полночь распадётся на два дня, минуты целые. Для графиков нужен свой регистр смен и разность дат с началом смены сотрудника.
Connect_Net возвращает Ложь с кодом −307 или −12001. Терминал недоступен: IP-адрес, порт 4370, межсетевой экран, подсеть. Код −6 — на терминале задан пароль связи; в SDK для него есть SetCommPassword, по практике его вызывают до подключения.DeleteAttlogBetweenTheDate, в 6.3.1.38 оба есть.EnableDevice(1, Ложь), а процесс прервался до включения. Подключитесь и вызовите EnableDevice(1, Истина) или перезагрузите терминал.В SDK есть ReadTimeGLogData(1, "2026-10-01 00:00:00", "2026-10-08 23:59:59") — интервал с точностью до секунды — и ReadNewGLogData(1). По руководству SDK оба работают только на прошивках новой архитектуры. Универсальный путь — читать всё и отсекать старое по дате, как в модуле выше.
SDK умеет события (RegEvent, OnAttTransactionEx), но для них процесс с объектом должен работать постоянно, а для 1С это неудобно. Учёту рабочего времени хватает загрузки раз в 10–15 минут.
Вместо Connect_Net — Connect_Com(ComPort, MachineNumber, BaudRate); для USB по руководству SDK нужен драйвер виртуального COM-порта. НомерТерминала() должен совпасть с номером устройства на линии.
Сначала сопоставьте коды терминала с сотрудниками в регистре СотрудникиСКУД. В КА 2.5 есть документ «Табель учета рабочего времени», но заполнять его из отметок — отдельная доработка. Своя обёртка над SDK вместо COM — тема статьи о внешней компоненте.
Если терминалов несколько, сервер 64-битный и правку реестра надо согласовать с администратором, а отчёт должен учитывать графики смен, быстрее отдать задачу программисту: он подберёт схему и проверит загрузку на копии базы. Разместите задачу — оценка бесплатная, ответ в течение рабочего дня. Отдельно отчёт на СКД — в разделе отчёты и обработки 1С.
Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.