Контроль активности пропуска

Version 5.1 by admin on 2026/06/07 18:49

Часто бывает полезно выявить неактивные пропуска, то есть пропуска, по которым давно не было никаких событий доступа. Для решения этой задачи в Платформе НЕЙРОСС реализован скрипт автоматизации, позволяет автоматически обнаружить неактивные пропуска и поместить их в отдельную группу, а также, при необходимости, — выполнить бессрочную приостановку пропуска. 

Контролируются не все пропуска, а только пропуска выбранных владельцев.

Посредством настройки свойств скрипта автоматизации вы можете:

  • задать имя группы для помещения пропусков;
  • определить, требуется ли автоматически выполнять изъятие пропусков, либо решение будет принимать оператор бюро пропусков;
  • задать период неактивности — временной интервал в днях, по истечении которого при отсутствии событий доступа пропуск считается неактивным;
  • указать, требуется ли учитывать факт изменения пропуска совместно с событием доступа — пропуск будет считаться неактивным, если за заданный период не было ни события доступа, ни факта изменения пропуска оператором бюро пропусков;
  • пометить, что пропуск приостановлен скриптом автоматизации.
Information

Задание автоматизации может запускаться оператором вручную, по расписанию (например, раз в неделю по воскресеньям), либо по какому-либо событию/системному действию.

Пример: Найти пропуска, по которым не было событий доступа в течение 60 дней и поместить их в группу «Устаревшие пропуска»

Задание автоматизации запускается автоматически ежедневно в 23:00, производится поиск за интервал в 60 дне. Найденные неактивные пропуска не приостанавливаются, остаются действительными и принадлежащими «своим» группам, но дополнительно помещаются в группу «Устаревшие пропуска». Анализируются только события доступа, факт изменения пропуска оператором АРМ Доступ не анализируется.

card_inactivity_scrypt.png


Настройки СКУД

Вам потребуется:

  1. Создать пользовательское свойство владельца пропуска для указания перечня лиц, для которых требуется проводить контроль активности пропуска.
  2. Добавить свойство на форму владельца пропуска и задать в значение true для владельцев, чьи пропуска требуется анализировать.
  3. Создать пользовательское свойство пропуска для пометок о том, что пропуск приостановлен скриптом.
  4. Добавить свойство на форму пропуска. 

Порядок действий:

Задача

Комментарий

Создание пользовательского свойства

В разделе Настройки СКУД АРМ НЕЙРОСС Доступ:

  1. На вкладке Свойства владельца пропуска добавьте новое свойство, задайте название и ключ свойства, укажите тип «Переключатель».
    card_inactivity_appprp.png
  2. На вкладке Свойства пропуска добавьте новое свойство, задайте название и ключ свойства, укажите тип «Переключатель».
    PausedByScrypt_addprp.png

Добавление пользовательских свойств

Изменение формы владельца пропуска

В разделе Персонал АРМ НЕЙРОСС Доступ выберите группу пропусков, для которой нужно настроить контроль активности, перейдите к вкладке Настройки группы:

  1. На вкладке Форма ввода: Владелец пропуска добавьте поле Контроль активности (название может быть любым) на форму владельца пропуска.
  2. На вкладке Форма ввода: Пропуск добавьте поле Приостановлен скриптом (название может быть любым) на форму пропуска.
Information

Рекомендуем использовать Конструктор форм. При использовании нестандартных форм, обратитесь к специалистам компании ИТРИУМ за услугой по доработке формы.

Настройка форм ввода данных

При необходимости вывода информации в таблице пропусков, на вкладке Настройка таблицы добавьте вывод данных свойств.

Настройка таблицы пропусков

Включение необходимости контроля активности пропуска

Выберите владельцев, для которых требуется проводить контроль активности пропуска, и в режиме редактирования пропуска / группы пропусков в поле Контроль активности установите переключатель в положение fa-on.pngВключено. Сохраните данные владельца.

card_inactivity_person_form.png

Управление пропусками

Просмотр результатов работы скрипта

Дождитесь выполнения условия запуска задания (в нашем примере это 23:00 текущего дня).

Настройки автоматизации

Скопируйте приведённый ниже код скрипта и создайте файл с произвольным именем и расширением JSON, например: задание_автоматизации_Контроль_устаревших пропусков.json. Для этого удобно использовать простые текстовые редакторы типа Блокнот или Notepad++. Вы также можете обратиться к специалистам компании ИТРИУМ, мы вышлем подготовленный файл.

    
   import play.api.libs.json.Format
   import beans.pacs.{PacsFolderBean, PassBean}
   import extensions.automation.scripts.AutomationActionScript
   import extensions.automation.signals.AutomationSignal
   import models.common.UserIdentity
   import models.neyross.{Pass, PassComponent}
   import models.pacs.{PacsFolder, PacsFolderComponent}
   import dto.pacs.PacsFolderMovementDto
   import services.logging.web.{LoggerWithWeb => Logger}
   import play.api.libs.json.{JsObject, Json}
   import proto.neyross.PassProto
   import services.common.{SystemLogService, SystemLogTag}
   import services.neyross.NeyrossEmbedApiService
   import slick.dbio.DBIOAction
   import utils.TableExtension
   import utils.bootstrap.CustomPostgresProfile
   import utils.common.ScalaUtils

   import java.time.OffsetDateTime
   import java.util.UUID
   import scala.concurrent.Future

   class ObsoletePassControlAutomationScript extends AutomationActionScript {

      val logger = Logger("ObsoletePassControlAutomationScript")

     private val postgresProfile = ctx.dbConfig.profile.asInstanceOf[CustomPostgresProfile]

     import postgresProfile.api._

      implicit val ec = ctx.executionContext
     private val neyrossEmbedApi = ctx.injector.instanceOf[NeyrossEmbedApiService]
     private val pacsFolderBean = ctx.injector.instanceOf[PacsFolderBean]
     private val systemLogService = ctx.injector.instanceOf[SystemLogService]
     private val passBean = ctx.injector.instanceOf[PassBean]

     private val folders = new PacsFolderComponent()(ctx.dbConfig)
     private val passes = new PassComponent()(ctx.dbConfig, neyrossEmbedApi, ec)

     // @parameter { "type": "string", "title": "Название группы для устаревших пропусков", "key": "workingFolder" }
     val workingFolder = "Устаревшие пропуска"

     // @parameter { "type": "string", "title": "Идентификатор свойства-признака контроля", "key": "shouldControlOutdateProperty", "description": "Контролироваться будут пропуска только тех владельцев, которые имеют данное свойство" }
     val shouldControlOutdateProperty = "inactivity"

     // @parameter { "type": "string", "title": "Значение свойства-признака контроля", "key": "shouldControlOutdatePropertyValue", "description": "Контролироваться будут пропуска только тех владельцев, которые имеют указанное значение данного свойства (true означает Да)" }
     val shouldControlOutdatePropertyValue = "true"

     // @parameter { "type": "select", "title": "Режим работы", "key": "mode", "options": [ { "value": "default", "label": "Помещение устаревших пропусков группу" }, { "value": "withStopping", "label": "Приостановка и помещение устаревших пропусков в группу" } ] }
     val mode = "withStopping"

     // @parameter { "type": "number", "title": "Максимальный период неактивности, дней", "key": "inactivityPeriod" }
     val inactivityPeriod = 1

     // @parameter { "type": "boolean", "title": "Анализировать время изменения пропуска", "key": "isUpdateTimeAnalysisEnabled", "description": "Если флаг установлен, при расчёте активности пропуска будут учитываться также события изменения пропуска оператором бюро пропусков. В противном случае будет анализироваться только время последнего прохода по данному пропуску" }
     val isUpdateTimeAnalysisEnabled = false

     // @parameter { "type": "string", "title": "Идентификатор свойства-маркера контроля", "key": "scriptMarkPropertyKey", "description": "При автоматической приостановке пропуска к нему будет добавлено свойство с данным идентификатором и значением true (Да). Вы можете вынести данное свойство на форму пропуска или в таблицу пропусков, при необходимости" }
     val scriptMarkPropertyKey = "PausedByScrypt"

      val modeLocalization: Map[String, String] =
        Map(
         "default" -> "Помещение устаревших пропусков в группу",
         "withStopping" -> "Приостановка и помещение устаревших пропусков в группу"
       )

      logger.debug(s"mode: ${modeLocalization.getOrElse(mode, "")}")
      logger.debug(s"modification time analysing: $isUpdateTimeAnalysisEnabled")
      logger.debug(s"inactivity period: $inactivityPeriod")

      override def onSignal(signal: AutomationSignal): Future[Unit] = {
        workingFolder.trim match {
         case "" =>
            logger.warn("working folder name is empty. do nothing")
            Future.unit
         case nonEmptyFolderName =>
            runWithNonEmptyFolderName(nonEmptyFolderName)
       }
     }

     private def createFolderWithNameAndParent(name: String, parent: Option[String]) = {
        val uuid = UUID.randomUUID().toString
        val folder = PacsFolder(
          id = None,
          uuid = Some(uuid),
          name = Some(name),
          parent = parent,
          isDefault = false,
          templates = None,
          defaultValues = None,
          settings = None,
          created = OffsetDateTime.now(),
          deleted = false
       )
        folders.insert(folder).map(_ => Some(uuid))
     }

     private def getFolderUuidByName(folderName: String) = {
        folders.folders
         .filter(f => f.name === folderName && !f.deleted)
         .result
         .headOption
         .flatMap({
           case Some(folder) =>
              DBIOAction.successful(folder.uuid)
           case _ =>
              folders
               .findDefault()
               .flatMap({
                 case Some(defaultFolder) if defaultFolder.uuid.nonEmpty =>
                    createFolderWithNameAndParent(folderName, defaultFolder.uuid)
                 case _ =>
                    logger.warn("there is no valid default folder. wtf?")
                    DBIOAction.successful(None)
               })
         })
     }

     private def isOutdated(timestamp: OffsetDateTime): Boolean = {
        val threshold = OffsetDateTime.now().minusDays(inactivityPeriod)
        logger.trace(s"checking if chosen timestamp $timestamp is older than threshold $threshold")
        threshold.isAfter(timestamp)
     }

     private def isStoppedOrInOutdatedFolder(
                                               pass: Pass,
                                               outdatedFolderUuid: String,
                                               passFolders: List[String]
                                            ): Boolean = {
        pass.getState.contains(PassProto.Pass.State.sSTOPPED) || passFolders.contains(outdatedFolderUuid)
     }

     private def isOutdatedPassRecord(record: PassRecord): Boolean = {
       // from all pass times, choose latest one and check if it outdated
       val lastAccessGrantedAtOption = record._2
        val created = record._3
        val lastUpdatedAtOption = record._4
        logger.trace(s"last access granted: $lastAccessGrantedAtOption, created: $created, last updated: $lastUpdatedAtOption")
       // in any cases, pass has "created" time
       var latestTimeToCheck: OffsetDateTime = created
       if (
          lastAccessGrantedAtOption.isDefined &&
            lastAccessGrantedAtOption.get.isAfter(latestTimeToCheck)
       ) {
         // last access event was later than created time - using it
         latestTimeToCheck = lastAccessGrantedAtOption.get
       }
       if (isUpdateTimeAnalysisEnabled) {
         if (
            lastUpdatedAtOption.isDefined &&
              lastUpdatedAtOption.get.isAfter(latestTimeToCheck)
         ) {
           // last update event was later than created time and last access time - using it
           latestTimeToCheck = lastUpdatedAtOption.get
         }
       }
        isOutdated(latestTimeToCheck)
     }

     private def getPassesToMove(records: Seq[PassRecord], outdatedFolderUuid: String): Seq[Pass] = {
        records
         .filter(record => {
            logger.trace(s"pass ${record._5.uuid} handling")
           !isStoppedOrInOutdatedFolder(record._5, outdatedFolderUuid, record._1.getOrElse(Nil)) &&
              isOutdatedPassRecord(record)
         })
         .map(_._5)
     }

      type PassFolders = Option[List[String]]
      type LastAccessGrantedAt = Option[OffsetDateTime]
      type Created = OffsetDateTime
      type LastUpdatedAt = Option[OffsetDateTime]
      type PassRecord = (PassFolders, LastAccessGrantedAt, Created, LastUpdatedAt, Pass)

      type PersonUUID = String
      type PersonFolders = Option[Seq[String]]
      type PersonRecord = (PersonUUID, PersonFolders)

     private def personToControlHandler(
                                          outdatedFolderUuid: String,
                                          personRecord: PersonRecord
                                       ): Future[Seq[String]] = {
        val dbAction =
          passes
           .extendedPasses4(
              TableExtension.folders(ctx.dbConfig),
              TableExtension.lastAccessGrantedAt(ctx.dbConfig),
              TableExtension.created(ctx.dbConfig),
              TableExtension.lastUpdatedAt(ctx.dbConfig)
           )
           .filter(r => r.person === personRecord._1 && !r.deleted)
           .result

        ctx
         .dbRun(dbAction)
         .flatMap(result => {
            logger.trace(s"found passes of person ${personRecord._1}: ${result.map(_._5.uuid)}")
            val passesToMove = getPassesToMove(result, outdatedFolderUuid)
            logger.trace(s"found outdated passes of person ${personRecord._1}: ${passesToMove.map(_.uuid)}")
           if (passesToMove.nonEmpty) {
              val isPersonInOutdatedFolder = personRecord._2.exists(_.contains(outdatedFolderUuid))
              val personsToMove = Option.when(!isPersonInOutdatedFolder)(personRecord._1).toList
              val dto = PacsFolderMovementDto(personsToMove, passesToMove.map(_.uuid))
              pacsFolderBean.copyToFolder(outdatedFolderUuid, dto)(ui = null, None).map(_ => passesToMove.map(_.uuid))
           } else {
              Future.successful(Nil)
           }
         })
     }

     private def getPersonsToControl: Future[Vector[PersonRecord]] = {
        val personsToControl =
          sql"""
    SELECT DISTINCT uuid, folders
      FROM
          neyross_person,
          jsonb_to_recordset((person_json->>'properties'):: jsonb) AS props(key text, value text)
      WHERE
          key = '#${shouldControlOutdateProperty}' AND
          value = '#${shouldControlOutdatePropertyValue}' AND
          neyross_person.deleted = false
     """
.as[PersonRecord]

        ctx.dbRun(personsToControl)
     }

     case class Property(key: String, value: String, index: Option[Long] = None)

      object Property {
        implicit val jsonFormat: Format[Property] = Json.format[Property]
     }

     private def stopPassesWithUuid(uuids: Seq[String]): Future[Unit] = {
        ctx
         .dbRun(passes.findByUuids(uuids))
         .flatMap(updatedPasses => {
            ScalaUtils
             .sequentialTraverse(updatedPasses)(updatedPass => {
                logger.trace(s"stopping pass ${updatedPass.uuid}; setting property $scriptMarkPropertyKey and disabled_from fields")
                val passJson = updatedPass.toFullJson.asOpt[JsObject].get
                val passProperties = (passJson \ "properties").asOpt[List[Property]].getOrElse(Nil)

                val newPassProperties = passProperties.filter(p => {
                  p.key != scriptMarkPropertyKey
               }) :+ Property(
                  scriptMarkPropertyKey,
                 "true"
               )
                val newPassJson = passJson ++ Json.obj(
                 "disabled_from" -> OffsetDateTime.now().toInstant.getEpochSecond.toInt,
                 "properties" -> Json.toJson(newPassProperties)
               )
                logger.trace(s"new pass json is: $newPassJson")
                passBean
                 .updateV2(updatedPass.uuid, newPassJson)(UserIdentity.getFakeUserIdentityFor("Автоматизация"), None)
             })
             .map(_ => ())
         })
     }

     private def runWithNonEmptyFolderName(folderName: String): Future[Unit] = {
        systemLogService
         .log(
            SystemLogTag.NORMAL,
            SystemLogTag.AUTOMATION
         )(s"Запущен процесс контроля устаревших пропусков")
         .map(_ => ())
        ctx
         .dbRun(getFolderUuidByName(folderName))
         .flatMap({
           case Some(folderUUID) =>
              logger.debug(s"working folder is $folderName ($folderUUID)")
              getPersonsToControl
               .flatMap(persons => {
                  logger.debug(s"found persons to control: $persons")
                  ScalaUtils.sequentialTraverse(persons)(personToControlHandler(folderUUID, _))
               })
               .flatMap(uuids => {
                  val flatUuidsList = uuids.flatten.distinct
                 if (flatUuidsList.nonEmpty) {
                    val stopping = if (mode == "withStopping") {
                      stopPassesWithUuid(flatUuidsList)
                   } else {
                      Future.unit
                   }
                    stopping.flatMap(_ => {
                      val yesOrNo = Option
                       .when(isUpdateTimeAnalysisEnabled)("да")
                       .getOrElse("нет")
                      logger.debug(s"were moved ${flatUuidsList.size} passes")
                      val message =
                        s"""В группу '$folderName' было помещено ${flatUuidsList.size} пропусков.
                           |Режим работы: ${modeLocalization.getOrElse(mode, "")}.
                           |Анализировать время изменения пропуска: $yesOrNo
                           |Максимальный период неактивности: $inactivityPeriod дней"""
.stripMargin
                      systemLogService
                       .log(
                          SystemLogTag.NORMAL,
                          SystemLogTag.AUTOMATION
                       )(
                          message = message,
                          data = Some(Json.toJson(flatUuidsList))
                       )
                       .map(_ => ())
                   })
                 } else {
                    logger.debug("not found persons with obsolete persons or passes")
                    systemLogService
                     .log(
                        SystemLogTag.NORMAL,
                        SystemLogTag.AUTOMATION
                     )(s"Не найдено владельцев для контроля устаревших пропусков, либо все пропуска таких владельцев не являются устаревшими")
                     .map(_ => ())
                 }
               })
           case _ =>
              Future.unit
         })
        Future.unit
     }
   }

   new ObsoletePassControlAutomationScript

Добавьте задание автоматизации

  1. В разделе Автоматизация нажмите на кнопку  Добавить новое задание btn_add_new.png, нажмите на кнопку Импорт и укажите путь к подготовленному на предыдущем этапе файлу [Автоматизация].
  2. В блоке Параметры сигнала выберите По расписанию, настройте расписание (например, 0 23 * * * для ежедневного запуска, или 0 23 * * 1 для запуска раз в неделю по воскресеньям). 
  3. В блоке Параметры действия настройте параметры задания автоматизации согласно таблице ниже.

    Параметр

    Комментарий

    Название группы устаревших пропусков

    Название группы. При запуске скрипта проверяется наличие группы с указанным именем в группе Все. При отсутствии, — она будет создана. В эту группу помещаться все найденные пропуска и их владельцы. Если название пустое, задание не выполняется. 

    Идентификатор свойства — признака контроля

    Уникальный идентификатор пользовательского свойства владельца пропуска, созданный на этапе #Настройки СКУД. В нашем примере: inactivity

    Значение свойства — признака контроля

    • true — если требуется проверять пропуска, для владельцев которого в поле Контроль пропусков переключатель установлен в положение fa-on.png Включено.
    • false — если требуется проверять пропуска, для владельцев которого в поле Контроль пропусков переключатель установлен в положение fa-off.png Выключено.

    В нашем примере: true

    Режим работы

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

    • Помещение устаревших пропусков в группу — просто помещать пропуска в дополнительную группу для принятия решения оператором о дальнейших действиях с пропуском. Пропуск остаётся действительным. Доступ по нему возможен.
    • Приостановка и помещение устаревших пропусков в группу — приостанавливать пропуск и помещать в дополнительную группу. Пропуск не удаляется из контроллеров доступа, но доступ блокируется.

    Максимальный период неактивности, дней

    Укажите временной интервал в днях (количество дней), по истечению которого пропуск без событий доступа считается устаревшим.

    Анализировать время изменения пропуска

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

    Идентификатор свойства-маркера контроля

    Уникальный идентификатор пользовательского свойства пропуска, значение которого будет устанавливаться в true для пропусков, которые приостановлены данным скриптом. В нашем примере: PausedByScrypt

    Алгоритм работы

    1. Поиск владельцев, для которых в поле с указанным идентификатором задано указанное значение.
    2. Поиск активных пропусков для найденных владельцев (если пропуск уже приостановлен или находится в группе устаревших пропусков операции с ним не выполняются).
    3. Поиск времени «активизации пропуска»:
      1. Если флаг в поле Анализировать время изменения пропуска установлен, то выбирается наиболее позднее из времени последнего редактирования и времени последнего прохода по пропуску
      2. Если флаг не установлен, выбирается время последнего прохода по пропуску.
    4. Если «время активизации» раньше чем текущее время минус Максимальный период неактивности, пропуск дополнительно помещается в группу устаревших пропусков.
    5. Если указан режим работы «Приостановка и перемещение устаревших пропусков в группу», все найденные пропуска бессрочно приостанавливаются. В поле Приостановлен с задаётся текущее время.
  4. Нажмите на кнопку Сохранить задание.
Error

ВАЖНО

В НЕЙРОСС пропуск не «привязывается» жёстко к одной группе. Принадлежность пропуска группе пропусков является просто свойством пропуска. Пропуск может принадлежать нескольким группам одновременно. При помещении пропуска в группу Устаревшие пропуска, пропуск остаётся также и в групе, в которой он располагался до запуска скрипта. Изъятие пропуска из группы осуществляется оператором АРМ Доступ с помощью команды Другие действия > Действие с группами > Изъять из текущей группы.

Запуск задания

Дождитесь выполнения условия запуска задания или запустите задание вручную. Для ручного запуска в поле Тип сигнала выберите значение По HTTP-запросу, сохраните задание и нажмите отправить запрос.

task_run_manually.png

Вы можете отслеживать процедуру инициализации и выполнения задания в Журнале аудита [Журнал аудита].

card_inactivity_scrypt_log.png

Запуск и выполненные скриптом действия фиксируются в системном журнале [Системный журнал].

card_inactivity_journal.png

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

card_inactivity_journal1.png

Пропуска приостанавливаются с текущего времени, свойство Приостановлен скриптом у приостановленных пропусков устанавливается в значение Да (true).

card_inactivity_card_form.png