Хабрахабр

Центр уведомлений. Приручаем 200+ рассылок

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

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

Вот такой фронт работ. Наша система отправляет 206 различных уведомлений из 35 систем девятью способами. Как для этой махины мы создавали единую коммуникационную платформу — центр уведомлений — рассказываем под катом.

Осознание задачи

Первым делом мы проанализировали все эти сообщения по следующим параметрам:

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

И вот какие результаты мы получили. Иными словами, мы хотели знать, что мы отправляем и кому.

Мы поняли, что хотим прийти к единому конвейеру уведомлений, где сообщения доставляются эффективно и сохраняют смысл.

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

И распространяет. В нашей концепции информационная система должна стать централизованной мини-редакцией с умным конвейером, который по теме/типу сообщения распознает, каким образом, когда, кому и по каким каналам его нужно распространить.

Если быть более конкретным, вот наши главные цели:

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

Обеспечить своевременную отправку. Иметь возможность настроить, как часто отправлять информацию и какой тип уведомления отправлять. 2. Скажем, информацию об отмене заказа нужно доставить как можно быстрее, а про распродажи и новые коллекции можно рассказывать раз в неделю. Уметь быстро добавить новое уведомление по запросу бизнеса.

Обеспечить гарантированную отправку информации. Важное сообщение рассылать самыми оперативно-просматриваемыми каналами — это смс, мессенджеры. 3. Мониторить и дублировать по этим же или другим каналам, если нет реакции. Или сразу всеми.

Сохранять единый стиль коммуникации. Создать систему коммуникации — шаблоны писем и сообщений — и придерживаться ее. 4. (Этому служит единый UI и редполитика).

Персонализировать коммуникацию. Различать, когда мы общаемся с физическим лицом, а когда с юридическим, в каком часовом поясе он живёт. 5.

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

Что с готовыми решениями?

На рынке есть коробочные решения, которые выполняют часть функций из тех, которые нам нужны. Например, в BizTalk можно вшить уведомления о событиях и настроить специальные пайпы, которые в конечном счете составляют это письмо. В Dynamics CRM и SharePoint есть настройки, которые реагируют на какие-то события. Как правило, это сводится к тому, что кто-то где-то нажал кнопку, и как результат — email отправился. О более сложных вещах, вроде политики агрегации и красивом UI, речи не идёт.

Получается, что весь контент для него нужно подготовить вручную. У Azure и AWS много сервисов, которые умеют взять готовый контент и отправить рассылку на почту. А назначение центра уведомлений — как раз в том, чтобы готовить контент.

Такие вещи не работают из коробки, их нужно делать руками. Так мы убедились, что в нашем случае не стоит уповать на решение из коробки, где нам останется настроить UI, и система сама куда-то подключится и вытащит всё нужное. У нас же получилось решение, сделанное под конкретные потребности бизнеса. Коробочные решения в таких случаях подразумевают использование мета-языка и конфигурирования, которые в конечном счете превращаются в проблему настройки. Его легко настроить и добавлять новые задачи.

Собираем конвейер. Архитектура нашего Центра уведомлений

У нас есть бизнес-системы — это системы, которые решают главные задачи бизнеса. Они работают отдельно от центра уведомлений и не должны заниматься вопросами коммуникации с пользователями.

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

Технически все это раскладывается в конвейер по переработке информации, который выглядит следующим образом:

1. Сборка событий

Все начинается со сборки событий, которая перехватывает или целенаправленно собирает данные из бизнес-систем и сохраняет их как «сырой» поток событий для дальнейшей обработки.

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

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

Now либо дата из пришедших бизнесовых данных, зависит от кейса), тип события и т.д., и все события сохраняются в общий поток событий для дальнейшей обработки. В момент сбора данные они приводятся к «канонической форме» — каждое событие получает уникальный идентификатор (либо берется CorrelationId, который уже был назначен событию в бизнес-системе), определяется дата события (DateTime. Форма конверта — каноническая, а содержимое разное. Сборщик не заинтересован непосредственно в данных события, он, образно говоря, оборачивает их в конверт.

2. Куда сохранять данные? EventStore

Как мы писали выше, в момент сбора событий они сохраняются в общий поток для дальнейшей обработки. Решая вопрос того, как хранить данные, мы выбрали широко известный в узких кругах продукт под названием EventStore. Кстати, одним из идеологов EventStore является Greg Young, имя которого неразрывно связано с такими концепциями как CQRS и Event Sourcing.

Если же попытаться сформулировать в свободной форме, что представляет из себя EventStore, то мы бы сказали, что это очень интересный гибрид базы данных и брокера сообщений. Согласно определению на главной странице, EventStore это «функциональная база данных с комплексной обработкой событий в JavaScript».

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

Это дает возможность не только реактивной работы с атомарными событиями, но и работы с массивами исторических данных, как в БД. Но, в отличие от брокеров сообщений, данные не удаляются из EventStore после того, как были прочитаны получателем. К слову, одной из самых продвигаемых фич Event Store является возможность построения temporal queries, позволяющих проводить анализ последовательностей событий во времени, что, как правило, сложно сделать с использованием «традиционных» БД.

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

генерировать агрегированные события в духе «Сводка о … за неделю/месяц/год»,
б. а. анализировать последовательности различных типов событий и находить интересные закономерности между ними, отслеживать пики происходящих событий и вообще.

3. Blob Storage

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

И самое простое, что можно сделать — прикрепить файл к письму в виде вложения. Простейший пример: отправка отчета по почте. Но если развивать сценарий, то мы сразу заметим несколько ограничений в таком решении.

  1. После отправки мы ничего не знаем о дальнейшей судьбе файла. Открывали ли его вообще, сколько раз, кто открывал.
  2. С почтой все легко, а вот в СМС-уведомление файл уже не вложишь.
  3. Дополнительная нагрузка на почтовый сервер при отправке файлов в виде вложений.
  4. Один и тот же файл может быть интересен множеству пользователей.

Простейшее решение всех этих вопросов — отправить вместо файла ссылку. Так мы и почтовый сервер не нагружаем, и можем мониторить, что происходит с файлом, и даже можем провести проверки безопасности и проверить, что пользователю действительно можно этот файл посмотреть.

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

Можно в самом начале сохранить файл в специальный сервис, а в событии сохранить его идентификатор. В общем, файлы могут существовать с самого начала жизни события, и нет смысла «протаскивать» тяжеловесный контент через весь конвейер Центра уведомлений только для того, чтобы в конце его отправить. А достать файл можно там, где он будет нужен уже при отправке уведомления.

Также в сервис можно послать GET-запрос с полученным Guid, а взамен он вернет файл. Так и родился у нас еще один очень простой сервис — Blob Storage.В сервис можно сохранить файл через POST-запрос и в ответ получить Guid. А все проверки безопасности, сбор статистики и прочее — это уже внутренние дела сервиса Blob Storage.

4. Обогащение данных

Итак, на данный момент мы рассмотрели этап сборки событий и то, как мы сохраняем информацию о событиях и файлах. Перейдем к этапу, на котором начинается работа с данными — этапу обогащения.

На выходе же мы получаем от нуля до N готовых уведомлений, которые уже имеют получателя, текст сообщения на конкретном языке, и уже понятно, по какому каналу (email, telegram, sms и т.д.) уведомление необходимо будет отправить. На вход данного этапа попадают «канонические» события, собранные ранее.

На данном этапе исходное событие может быть подвергнуто следующим манипуляциям.

Дополнение исходного набора данных

В основном благодаря этой операции весь этап и получил название «обогащение». Простейший пример дополнения данных — в «каноническом» событии нам пришел Id пользователя. На данном этапе мы по Id пользователя находим его ФИО, чтобы иметь возможность обратиться к нему по имени в тексте уведомления, и дополняем исходное событие найденной информацией.

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

Агрегация, обработка последовательностей событий, анализ во времени

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

После выполнения обогащения наступает последний этап работы с событием:

5. Генерация уведомлений

Уведомление отличается от события тем, что это не просто JSON-данные, а уже совершенно конкретное сообщение с известным получателем, каналом отправки (email, sms и т.д.), и текстом уведомления.

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

В итоге из одного события мы получаем от 0 до N уведомлений. По своей сути, это простой join входящего события с подписками пользователей на данный тип события и прогон исходных JSON-данных через тот или иной шаблонизатор для генерации текста сообщения для указанного в подписке канала.

Но это уже все очевидные и неинтересные вещи, и мы не будем тратить на них внимание читателей. Дальше дело за простым — распихать полученные уведомления по соответствующим потокам, откуда их вычитают микросервисы, занимающиеся отправкой писем, sms сообщений и т.д.

6. Подписки

Последний модуль из тех, что мы рассмотрим — модуль настройки подписок пользователями.

Идея простая — это база данных, которая хранит информацию о том, кто и в каком типе уведомлений заинтересован + UI для настройки.

Для каждого типа уведомления доступен один или несколько каналов отправки. Когда пользователь хочет подписаться, он выбирает интересный ему тип уведомления и открывает окно настроек. Тут пользователь указывает минимальное количество настроек: свою почту для email или свой номер телефона для Telegram и при желании выполняет отправку тестового сообщения, чтобы убедиться, что все настроено верно.

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

7. Разграничение доступа

Еще один момент, который мы не рассмотрели ранее, это разграничение доступа. Объясним на примере. Допустим, у нас есть уведомление о том, что контрагенту пора внести оплату за товар. Данное уведомление относится к конкретному контрагенту и получить его должен он один, а не все контрагенты. Соответственно, одного только типа уведомления, описанного выше, недостаточно для того, чтобы определить, для кого конкретно предназначено событие.

И при определении получателей для события отсекать не только по типу события, но и по коду контрагента. Самое простое, но и самое неудачное решение — внести в логику Центра уведомлений понятие контрагента, у которого есть, к примеру, код.

Центр уведомлений появился как отдельная система, чтобы избавить бизнес-системы от задачи отправки уведомлений. Почему это плохо? Но, внося подобные детали в Центр Уведомлений, мы со временем наполним уже Центр Уведомлений кучей знаний из бизнес-систем.

Правильное решение данного вопроса в нашем случае все равно оказалось очень простым, но гибким.

Claim по своей сути это просто пара «идентификатор-значение». Для авторизации в наших системах мы используем ADFS, то есть авторизация на основе claims. Ключи этих типов claims заранее известны и стандартны для всех приложений. Есть заранее определенный набор claims — имя пользователя, набор claims с ролями пользователя и т.д. Также мы используем custom claim providers для присвоения пользователям различных специфичных атрибутов при авторизации.

Или код региона, в котором находится пользователь. Например, упомянутый выше код контрагента, если данный пользователь является контрагентом, можно сохранить в виде claim с собственным ключом. Мы можем сохранить таким образом любые значимые маркеры. Или владельцем какого девайса он является, IPhone или Android.

Это уже половина решения задачи — у нас есть набор claims для каждого пользователя, а значит, и подписки.

Если мы добавим claims к нашим событиям, то мы можем сопоставлять события и подписки не только по типу, но и проверять, что и у события, и у подписки claims с одними и теми же ключами имеют одни и те же значения.

Нам не нужно вносить в Центр уведомлений никакой специфики бизнесовых систем. Обратите внимание, понятие «событие, у которого есть claim X со значением Y» — это куда более абстрактная вещь, чем «событие, предназначенное для контрагента Y».

Поскольку генерация Claims — это все-таки бизнес-специфика в духе «у события о необходимости оплаты должен быть claim контрагента», мы разместили данную логику на этапе сборки данных. Вопрос остается в том, где заполнять эти Claims для событий. То есть он уже имеет некоторый бизнес-контекст. Сборщик как минимум знает, откуда брать событие и его структуру.

Соответственно, сборщик может взять код контрагента из нужного поля контракта и абстрагировать его до понятия «claim с ключом X и значением Y» перед отправкой события в конвейер Центра уведомлений. На примере события о необходимости внесения оплаты, код контрагента чаще всего находится в контракте бизнес-события о необходимости оплаты, которое сборщик перехватил.

Итого

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть