
Во многих мобильных приложениях, которые позволяют узнавать штрафы и оплачивать их, есть возможность получать информацию о новых штрафах. Для этого удобно использовать отправку 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 приложениях.

