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

Наше приложение по оплате штрафов не стало исключением. Серверная часть у нас реализована на платформе Ensemble, в которой с версии 2015.1 очень вовремя появилась встроенная поддержка push-уведомлений.

Для начала немного теории


Push-уведомления — это один из способов распространения информации, когда данные поступают от поставщика к пользователю на основе установленных параметров.

В общем случае для мобильных устройств процесс уведомления выглядит так:



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

Для работы с push-уведомлениями в Ensemble есть следующие сущности:

» EnsLib.PushNotifications.GCM.Operation — бизнес-операция для отправки push-уведомлений на сервер Google Cloud Messaging Services (GCM). Операция также позволяет отправлять одно сообщение приложению сразу на несколько устройств.

» EnsLib.PushNotifications.APNS.Operation — бизнес-операция, которая отправляет уведомление на сервер Apple Push Notifications. Для отправки сообщений в каждое реализованное приложение понадобится отдельный SSL сертификат.

» EnsLib.PushNotifications.IdentityManager — бизнес-процесс Ensemble. Позволяет отправлять сообщения пользователю, не задумываясь о количестве и типах его устройств. По сути, Identity Manager содержит таблицу, ставящую в соответствие одному идентификатору пользователя все его устройства. Бизнес-процесс Identity Manager’а получает сообщения от других компонентов продукции и перенаправляет их маршрутизатору, который в свою очередь рассылает все GCM-сообщения в GCM-операцию, и каждое APNS-сообщение в APNS-операцию, сконфигурированную с соответствующим SSL сертификатом.

» EnsLib.PushNotifications.AppService – бизнес-служба, позволяющая отправлять push-сообщения, сгенерированные вне продукции. По сути, само сообщение может генерироваться где-то внутри Ensemble независимо от продукции, служба же позволяет отправлять эти сообщения из Ensemble. Подробно все эти классы описаны в разделе документации Ensemble "Configuring and Using Ensemble Push Notifications".

Теперь о том, как процесс уведомлений реализовали мы


В нашем случае сообщения генерируются специально разработанным бизнес-процессом внутри продукции, поэтому служба нам не пригодилась. Также на данном этапе у нас имеется только Android-приложение, поэтому APNS-операцией мы тоже пока не пользовались. По сути мы использовали самый низкоуровневый способ отправки напрямую через GCM-операцию. В дальнейшем, при реализации iOS-версии приложения, удобно будет реализовать работу с уведомлениями через Identity Manager, чтобы не пришлось анализировать тип и количество устройств. Но сейчас расскажем подробнее о GCM.

Для отправки уведомлений необходимо реализовать процесс внутри продукции и подключить нужную бизнес-операцию. На текущий момент у нас два отдельных процесса отправки Push-уведомлений, каждый со своей логикой: уведомления по новым штрафам, уведомления о окончании действия скидки по штрафу. О каждом типе расскажем немного подробнее.

Сначала о общей схеме данных и настройках, необходимых для работы всех уведомлений.

  • Создаем пустую SSL конфигурацию для работы операции, добавляем ее в конфигурацию бизнес-операции (только для GCM!).
  • Добавляем в продукцию операцию класса EnsLib.PushNotifications.GCM.Operation, настраиваем ее параметры:

NotificationProtocol: HTTP
PushServer: http://android.googleapis.com/gcm/send

Настройки операции в итоге выглядят так:


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

Client – для хранения клиентов, App – для хранения устройств, Doc – для хранения данных документов:

Class penalties.Data.Doc Extends %Persistent
{
///тип документа (СТС или ВУ)
Property type As %String;
///идентификатор документа
Property value As %String;
}
Class penalties.Data.App Extends %Persistent
{
///тип устройства (GCM или APNS)
Property Type As %String;
///идентификатор устройства 
Property ID As %String(MAXLEN = 2048);
}
Class penalties.Data.Client Extends %Persistent
{
/// почтовый адрес клиента из Google Play Services, используем как идентификатор
Property Email As %String;
///список устройств клиента
Property AppList As list Of penalties.Data.App;
///список документов, на которые подписался клиент
Property Docs As list Of penalties.Data.Doc;
}

Для рассылки уведомлений о новых штрафах нам надо понимать, какие штрафы клиенту отправлять, а какие он уже видел, при входе в приложение. Для этого у нас есть класс NotificationFlow, в котором мы отмечаем, что клиент уже получал информацию о штрафе.

Class penalties.Data.NotificationFlow Extends %Persistent
{
///идентификатор клиента (в нашем случае email)
Property Client As %String;
///идентификатор штрафа
Property Penalty As %String;
/// признак отправки
Property Sent As %Boolean;
}

Для удобства восприятия ниже при упоминании классов опустим имена пакетов. По содержанию классов понятно, как будет выглядеть процесс по новым штрафам: для каждого клиента проходим по списку документов, делаем по ним запрос штрафов в ГИС ГМП (Государственная информационная система о государственных и муниципальных платежах), проверяем полученные штрафы на наличие в NotificationFlow, если найдены – удаляем из списка, в итоге формируем список штрафов, о которых надо уведомить клиента, пробегаемся по списку устройств клиента и отправляем на каждое из них push уведомление.

Верхний уровень:


где clientkey – свойство контекста, значением по умолчанию которого является идентификатор первого по порядку клиента имеющего подписку, хранящегося в классе Client.

Подпроцесс выглядит так:


Заглянем внутрь блоков foreach:


После этого блока foreach имеем готовый запрос EnsLib.PushNotifications.NotificationRequest, в который осталось добавить текст сообщения. Это делаем в блоке foreach по Doc’ам.


И небольшой кусок кода, заполняющий данные запроса:

ClassMethod getPenaltyforNotify(client As penalties.Data.Client, penaltyResponse As penalties.Operations.Response.getPenaltiesResponse, notificationRequest As EnsLib.PushNotifications.NotificationRequest)
{
    set json="",count=0
    set key="" for  
    {
        set value=penaltyResponse.penalties.GetNext(.key)
        quit:key=""
        set find=0
        set res=##class(%ResultSet).%New("%DynamicQuery:SQL")
        set exec="SELECT * FROM penalties_Data.NotificationFlow WHERE (Penalty = ?) AND (Client = ?)"
        set status=res.Prepare(exec)
        set status=res.Execute(value.billNumber,client.Email)
        if $$$ISERR(status) do res.%Close() kill res continue
        while res.Next()
        {
            if res.Data("Sent") set find=1
            }
        do res.%Close() kill res
        if find {do penaltyResponse.penalties.RemoveAt(key), penaltyResponse.penalties.GetPrevious(.key)}
        else  {
            set count=count+1
            do notificationRequest.Data.SetAt("single","pushType")
            for prop="billNumber","billDate","validUntil","amount","addInfo","driverLicence","regCert"
            {
                set json=$property(value,prop)
                set json=$tr(json,"""","")
                if json="" continue
                do notificationRequest.Data.SetAt(json,prop)
                    
            }
            set json=""
            set notObj=##class(penalties.Data.NotificationFlow).%New()
            set notObj.Client=client.Email
            set notObj.Penalty=value.billNumber
            set notObj.Sent=1
            do notObj.%Save()
        }
    }
    if count>1 {
        set keyn="" for {
            do notificationRequest.Data.GetNext(.keyn)
            quit:keyn=""
            do notificationRequest.Data.RemoveAt(keyn)
        }
        do notificationRequest.Data.SetAt("multiple","pushType")
        do notificationRequest.Data.SetAt(count,"penaltiesCount")
    }
}

Процесс по скидкам на оплату штрафов реализован несколько иначе. На верхнем уровне:


Отбор штрафов со скидкой выполняется следующим кодом:

ClassMethod getSaleforNotify()
{
    //на всякий случай почистим временную глобаль
    kill ^mtempArray
    set res=##class(%ResultSet).%New("%DynamicQuery:SQL")
    //поищем все еще не оплаченные штрафы со скидкой
    set exec="SELECT * FROM penalties_Data.Penalty WHERE status!=2 AND addInfo LIKE '%Скидка%'"
    set status=res.Prepare(exec)
    set status=res.Execute()
    if $$$ISERR(status) do res.%Close() kill res quit
    while res.Next()
    {
        set discDate=$piece(res.Data("addInfo"),"Скидка 50% при оплате до: ",2)
        set discDate=$extract(discDate,1,10)
        set date=$zdh(discDate,3)
        set dayscount=date-$p($h,",")
        //отправлять будем за 5,2,1 и 0 дней
        if '$lf($lb(5,2,1,0),dayscount) continue
        set doc=$s(res.Data("regCert")'="":"sts||"_Res.Data("regCert"),1:"vu||"_Res.Data("driverLicence"))
        set clRes=##class(%ResultSet).%New("%DynamicQuery:SQL")
        //поищем клиентов, подписанных на документ
        set clExec="SELECT * FROM penalties_Data.Client WHERE (Docs [ ?)"
        set clStatus=clRes.Prepare(clExec)
        set clStatus=clRes.Execute(doc)
        if $$$ISERR(clStatus) do clRes.%Close() kill clRes quit
        while clRes.Next()
        {
            //составим удобный список, по которому потом будем бегать
            set ^mtempArray($job,clRes.Data("Email"),res.Data("billNumber"))=res.Data("billDate")
        }
        do clRes.Close()
    }
    do res.Close()
}

На выходе имеем глобал с разбивкой штрафов по клиентам. Теперь надо пробежаться по этому глобалу и отправить каждому клиенту его штраф, предварительно убедившись, что он еще не оплачен где-нибудь еще:


Проваливаемся в цикл по штрафам:


Собственно разница между процессами в следующем: в первом случае обязательно пробегаемся по всем нашим клиентам, во втором отбираем только клиентов, у которых есть штрафы определенного вида; в первом случае для нескольких штрафов шлем одно уведомление с общим количеством (бывают клиенты, которые за день успевают нахватать много штрафов), во втором случае по каждой скидке отдельно.
В процессе отладки столкнулись с небольшой особенностью наших сообщений, из-за которой некоторые системные методы нам пришлось переопределить. Одним из параметров нашего сообщения мы передаем номер штрафа, который в общем виде выглядит примерно так «12345678901234567890». Системные классы операции по отправке уведомлений преобразуют такие строки в числа, а GCM сервис, к сожалению, получив такое большое число недоумевает и возвращает «Bad Request».

Поэтому переопределили системный класс операции, в нем вызываем свой метод ConvertArrayToJSON, внутри которого вызываем ..Quote со вторым параметром равным 0, то есть не преобразовываем строки, состоящие только из цифр в числа, а оставляем их строками:

Method ConvertArrayToJSON(ByRef pArray) As %String
{
#dim tOutput As %String = ""
#dim tSubscript As %String = ""
For {
    Set tSubscript = $ORDER(pArray(tSubscript))
    Quit:tSubscript=""
    Set:tOutput'="" tOutput = tOutput _ ","
Set tOutput = tOutput _ ..Quote(tSubscript) _ ": "
    If $GET(pArray(tSubscript))'="" {
        #dim tValue = pArray(tSubscript)
        If $LISTVALID(tValue) {
            #dim tIndex As %Integer
            // $LIST .. aka an array
            // NOTE: This only handles an array of scalar values
            Set tOutput = tOutput _ "[ "
            For tIndex = 1:1:$LISTLENGTH(tValue) {
                Set:tIndex>1 tOutput = tOutput _ ", "
                Set tOutput = tOutput _ ..Quote($LISTGET(tValue,tIndex),0)
            }
            Set tOutput = tOutput _ " ]"
        } Else {
            // Simple string
            Set tOutput = tOutput _ ..Quote(tValue,1)
        }
    } Else {
        // Child elements
        #dim tTemp
        Kill tTemp
        Merge tTemp = pArray(tSubscript)
        Set tOutput = tOutput _ ..ConvertArrayToJSON(.tTemp)
    }
}
Set tOutput = "{" _ tOutput _ "}"
Quit tOutput
}

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

  • добавить нужную операцию
  • выстроить процесс, заполняющий следующие свойства запроса: AppIdentifier — Server API Key, полученный при регистрации сервиса в GCM, Identifiers — список идентификаторов устройств, к которым обращаемся, Service — тип устройства, к которому обращаемся (в нашем случае «GCM»), Data — сами данные запроса (помним, что массив строится по принципу ключ-значение).

Собственно, все. За счет использования готовых компонентов Ensemble реализация процесса занимает пару часов, включая отладку и тестирование.

На выходе имеем довольных клиентов, своевременно узнающих о новых штрафах и вовремя вспоминающих о скидках.


Посмотреть как это работает можно в Android и iOS приложениях.
Поделиться с друзьями
-->

Комментарии (14)


  1. BupycNet
    25.05.2016 20:01
    +2

    А чем вы получаете инфу о штрафах? Хотелось бы в PushAll сделать канал по штрафам, я смотрю много где реализовано, это какой то открытый API?


    1. alt3rmann
      25.05.2016 22:34

      Возможно через ЕМП: http://dit.mos.ru/apps/dev
      По идее, в их сервисе не должно быть привязки только к Москве, так как в первоначальном сервисе в СМЭВ, на сколько я помню, такой привязки нет.


    1. medvedevia
      25.05.2016 23:55
      +1

      Мне из госуслуг смска приходит, зачем еще какие-то приложения?


      1. vanilla_men
        26.05.2016 00:14
        +1

        Не все госуслугами пользуются)


    1. vanilla_men
      26.05.2016 00:05
      +2

      Мы пользуемся ГИС ГМП. Получаем оттуда информацию.
      В ДИТ есть информация о московских штрафах, но зачастую идет сильный рассинхрон с ГИС ГМП. Парсить сайт ГИБДД — там тоже наблюдается рассинхрон при оплатах штрафов. Поэтому чтобы сделать Push уведомления — дергать клиентов, лучшего источника не найти (всегда актуально, можно понять оплачен штраф или нет).


  1. Andrew51130
    25.05.2016 23:55

    В чем суть статьи? Рассказать о миллионе Ваших внутренних сущностей?
    GCM имеет вполне простую и адекватную документацию на русском(даже..) языке, и делов там на полчаса от силы с перекурами.
    Есть адрес, есть формат данных для отправки push-ей. Пара идентификаторов и все.
    Считаю, статья не про раздел «android_dev»


    1. vanilla_men
      26.05.2016 00:00
      +1

      Статья — о том, как реализовать не через HTTP, а средствами Ensemble, абстракцией от HTTP. Чтобы не писать кучу кода. Тут больше про шинное взаимодействие — где GCM используется как готовая абстракция. Практически живые примеры привели, потому что документации очень мало.


      1. Calc
        26.05.2016 00:27
        +1

        Чтобы не писать кучу кода.

        не совсем понял к чему эта фраза относится
        вот нарпимер через HTTP этой «кучи» достаточно более чем (богомерзкий php)
        private function send(Message $message, $api_key){
                $ch = curl_init();
                curl_setopt($ch, CURLOPT_URL, self::URL);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER,true);
                curl_setopt($ch, CURLOPT_HTTPHEADER, array(
                    "Authorization: key=$api_key",
                    'Content-Type: application/json'
                ));
                curl_setopt($ch, CURLOPT_POST, true);
                curl_setopt($ch, CURLOPT_POSTFIELDS, Helpers::toJson($message));
                $result = curl_exec ($ch);
                ///...
            }
        

        Абстрагированием от http является наличие класса Message. Заполнение любое. из функции возвращать можно любой объект, по желанию.


  1. vanilla_men
    26.05.2016 00:47
    +2

    PHP это хорошо, но в данном контексте не совсем понятно? Мы пользуемся шиной данных, и рассказываем как это делается на шине) Можно это делать на любом языке, в любой среде.
    Тут же упор делаем о вписании такого функционала в механизм использования «Бизнес операций» в Ensemble (с очередями сообщений, обработкой ошибок, приоритетами доставки, конфигурациями и т.п.). Чуть больше о возможностях — в описании родительского класса


  1. morisson
    26.05.2016 11:36

    Посмотреть как это работает можно в Android и iOS приложениях.

    Про ваше iOS приложение уже была заметка ;)


  1. geronix
    26.05.2016 17:01

    Подскажите пожалуйста в чем вы рисовали блок-схемы?


    1. vanilla_men
      26.05.2016 17:04
      +1

      Это так выглядят процессы в Ensemble. Рисуешь блок схему, настраиваешь блоки, и запускаешь. Как бы метапрограммирование — исполнение бизнес процесса.


    1. morisson
      26.05.2016 17:24

      Это интерактивный инструмент редактирования бизнес-процессов в InterSystems Ensemble. Как «нарисовал», так потом и работать будет.


    1. intersystems
      26.05.2016 20:35

      Вот видео как работать в редакторе бизнес-процессов Ensemble.