Как Fix Price автоматизировал бизнес-процессы с помощью Camunda
Привет, Хабр! Я Вадим Райский, руководитель на IT-проектах Fix Price. Сегодня я расскажу вам об оптимизации бизнес-процессов, которую наша команда выполнила при помощи движка Camunda.
Чтобы сократить время и финансовые затраты на решение корпоративных задач (например, открытия новых магазинов), необходимо периодически корректировать бизнес-процессы: что-то добавлять, что-то, наоборот, удалять. В качестве примера разберем одну из последних возникших перед нами задач: автоматизировать процесс обработки строительно-ремонтных заявок и смет через наше приложение «Смета». Здесь можно было пойти несколькими путями: разработать это с нуля, использовать готовые решения или low-code решения. В результате мы пришли к внедрению движка Camunda, но сначала о том, почему было принято такое решение.
Костяк веб-приложения составляет многим известный фреймворк Yii2 (PHP). Жизненный цикл заявки управляется стейт-машиной. Для каждого состояния заданы возможные действия с документом и ограничен круг ролей, которым это позволено. Для реализации функционала было написано значительное количество строк кода. От бизнеса пришло пожелание оптимизировать управление бизнес-процессом, потому что:
Бизнес-процесс разрастается, в будущем станет еще более сложным, поэтому желательно иметь актуальное визуальное представление всей схемы.
Хочется гибко управлять бизнес-процессом, не переписывая код бэкенда (далее опишу, получилось ли у нас это, и насколько).
У аналитиков, разработчиков и тестировщиков складывается полное понимание, как работает сервис, именно при работе со схемой бизнес-процесса.
Понятно, что оптимальным вариантом для решения всех этих задач виделась именно Camunda. Для тех, кто не знаком с этим движком, дам краткое описание. Camunda — это движок управления бизнес-процессами (BPM). Бизнес-процесс наглядно моделируется в виде схемы BPMN, на которой течение процесса проходит через задачи-квадраты и может разветвляться по условиям. Это хорошо видно на следующем скрине:
В составе «Камунды» есть приложение TaskList для организации обработки пользователями входящих задач (UserTask). Для пользовательского ввода можно настраивать формы (Forms). Camunda умеет работать с таблицами принятия решений (DMN), где переменные пропускаются через набор бизнес-правил. Интегрируется с Keycloak (об этом решении мы уже рассказывали пару лет назад) и имеет систему уровней доступа, чтобы каждой группе пользователей приходили соответствующие им задачи. На первый взгляд, система выглядит так, что на ее базе можно настроить и логику, и интерфейс. Но на практике всё оказалось иначе.
Схема интеграции
Для консультаций коллеги поделились с нами ссылкой в профильный чат в Телеграме, где мы договорились о созвоне с известным спецом Денисом Котовым. Денис оказал неоценимую помощь нашей команде, подробно расписав следующие тезисы:
Обработку/валидацию пользовательского ввода выбрасываем за борт «Камунды», потому что в процессе эксплуатации всплывет неудобство API Camunda для работы с ним.
Переменные контекста, особенно если их много, тоже выбрасываем, потому что на большом количестве запущенных процессов это дело начинает тормозить на джоинах внутренних таблиц движка. (На наших масштабах скорее всего неактуально, но схема ниже покажет, как жить без этих переменных.)
Формы ввода и TaskList тоже выбрасываем, потому что там сложно спроектировать нужный нам фронтенд.
Авторизацию и права доступа… правильно, выбрасываем, так как система прав плоская и не слишком удобная.
И от таблиц принятия решений (DMN) тоже отказываемся, потому что как минимум существует проблема с их дебагом (проблем на самом деле больше, но и этой вполне достаточно).
Что же остается? Собственно, самая главная функция, ведь Camunda — она про следование по бизнес-процессу, и потому из коробки мы берем только это.
Пользовательские задачи будем обрабатывать через External Tasks, прибегнув к некоторым хитростям.
Схема интеграции бэкенда PHP и Camunda при описанных выше вводных приобретает такой вид:
Глоссарий для понимания схемы:
ProcessDefinitionKey — ключ описания процесса; однозначно задает, с экземпляром какого процесса имеем дело. В нашем случае определяет и тип сущности, над которой производятся действия;
BusinessKey — идентификатор сущности, над которой выполняется данный экземпляр процесса;
ServiceTask — задача, которая должна быть обработана определенным кодом автоматически;
ExternalTask — подвид ServiceTask, который обрабатывается внешним сервисом (например, кодом на бэкенде PHP);
Topic — подобие топика в очередях — ключ, который можно назначить нескольким ExternalTask на схеме и обрабатывать их общим хендлером;
UserTask — так на схеме обозначен ExternalTask, который требует взаимодействия с пользователем (не путать с термином из «Камунды»);
UserTaskModel — хранимая в базе данных бэкенда запись о задаче, требующей пользовательского ввода;
ExternalWorker — воркер бэкенда, забирающий из Camunda и исполняющий таски;
ExternalTaskId — идентификатор задачи, по которому нужно сообщить Camunda, что задача выполнена или упала с ошибкой.
На бэкенде в фоне крутится воркер, который запрашивает у Camunda задачи по REST API. Обычные сервис-таски он обрабатывает сразу и отправляет в Camunda запрос на complete, если задача выполнена успешно, или failure, если произошла ошибка. Таски для пользователей в BPMN кодируются нами как те же ExternalTask, но ассоциированный с ними хендлер только сохраняет в БД бэкенда UserTaskModel с ProcessDefinitionKey, BusinessKey и ExternalTaskId. Сюда еще можно добавить ActivityId — уникальный в пределах процесса идентификатор квадратика/этапа исполнения. И тогда получается, что само наличие в БД некоторой UserTaskModel означает, что данная сущность в данном месте бизнес-процесса ждет реакции пользователя.
В БД бэкенда мы можем делать произвольные выборки сущностей, поджойнив их с пользователями и правами, а также с существующими записями UserTaskModel. Следовательно, для каждого пользователя уже возможно получить список входящих задач, не используя штатный TaskList. Получив задачу, пользователь в нашем фронтенде заполняет наши формы и отправляет результат в наш бэкенд. Camunda ни о чем этом не знает, то есть мухи отделены от котлет. Далее, бэкенд сообщает Camunda, что задача с данным ExternalTaskId выполнена, а UserTaskModel удаляется. Итог: Camunda только раздает задачи и получает запросы о движении дальше. В сухом остатке имеем четкое разделение областей ответственности.
Для полноты картины покажу, как разруливать условные ветвления на гейтвеях так, чтобы решения принимала Camunda, а знания для этих решений остались на бэкенде.
На схеме выше стрелочки, выходящие из гейтвеев, срабатывают по условиям, описанным на специальном языке выражений. Бизнес-процесс идет по той стрелке, у которой выражение логически разрешилось в True. Фокус оказался в том, чтобы в выражение подставить вызов сервиса, который вычисляет ответ, а сам сервис запрашивает информацию для принятия решения у бэкенда.
Итого, контекст процесса в BPMN не содержит данных, оперируя только BusinessKey. Вообще вся логика принятия решений может оставаться на бэкенде, либо всё же содержаться в выражении, но данные для подстановки в него все равно будут жить на бэкенде.
О бэкенде подробнее
Для запросов в REST API Camunda мы взяли OpenAPI-спецификацию, сгенерировали PHP-код клиента и положили в composer-пакет.
Из всего API нам понадобились следующие классы, поля и методы:
При создании бизнес-сущности мы запускаем экземпляр процесса, передав ему businessKey. Далее нам остается ждать, когда в REST API появятся ассоциированные с ней задачи.
Первая прикидка воркера выглядела примерно так:
Воркер получает задачи (при запросе можно не указывать конкретные топики, тогда воркер получит задачи из всех топиков сразу) и передает их диспетчеру. Задача диспетчера — выяснить, какие хендлеры обрабатывают задачу, которая пришла с данными processDefinitionKey, activityId и topicName. Задача хендлеров — выполнить действия.
Сервис-таски можно отделить от юзер-тасков, назначив им разные топики. Complete или failure выполняют либо хендлеры (и тогда это действие в хендлере юзер-тасков пропускается), либо воркер (но тогда воркер должен знать, какой топик назначен юзер-таскам). Хендлер юзер-таска отвечает за сохранение нужных нам полей из LockedExternalTaskDto в UserTaskModel.
В эндпоинтах пользовательского ввода используется CamundaServiceInterface с тремя нужными методами: старт процесса, комплит-задачи и фейл-задачи:
Реализация прячет в себе поиск UserTaskModel, чтобы эта деталь не просачивалась в юзерспейс. В каждом эндпоинте бэкенда мы статически знаем, в каком процессе он фигурирует и какая задача будет выполнена при его вызове. Также известен ключ сущности, так что передаем эту троицу в методы.
Если запросы к Camunda на complete (задача выполнена) и failure (задача упала) отправлять асинхронно (через очередь), наше приложение становится устойчивым к падениям Camunda. С точки зрения пользователя эта авария будет выглядеть, как постепенное исчерпание входящих задач, а после восстановления работоспособности — появление новых. Соответственно, в боевых условиях используется асинхронная реализация через очередь, а в acceptance-тестах — синхронная.
О Camunda более подробно
Изначально у нас была поднята standalone-версия Camunda 7.21, но этот вариант после обдумывания новой схемы был отброшен в пользу Spring Boot приложения, так как это позволяло добавлять туда свой код.
Напомню, что со стороны Camunda задача состояла в том, чтобы держать логику на бэкенде, оставив Camunda только выбор дальнейшего пути. Исходящие из гейтвея ветви исполнения в Camunda Modeler имеют атрибут Condition, где нас интересует тип Condition Expression:
Выглядит логичным составлять это выражение из переменных, добавленных в контекст. Но у этого подхода есть недостатки:
бизнес-логика просачивается в область управления процессом;
в выражении легко допустить ошибку, и это всплывет только на проде;
покрыть логику тестом становится сложнее, чем просто написать юнит-тест и положить его в коде бэкенда.
Денис Котов предложил инкапсулировать всё выражение вызовом некоторого метода с бэкенда, возвращающего Boolean. Если бы всё приложение вместе с бэкендом и Camunda жило в одном Spring-приложении, это был бы очевидный ход. Идею мы поняли, но всё же разрешили себе выражения вида ${entity.type == «emergency»}. Оставалось затянуть знания с PHP-бэкенда, минуя контекст.
Ввиду отсутствия достаточной экспертизы в Java, трюк с обратными запросами из Camunda к бэкенду был сделан максимально «в лоб». Из документации и опыта знакомства с Spring использованы следующие факты:
Camunda можно интегрировать как зависимость в Spring Boot приложении;
Язык условных выражений поддерживает обращение к бинам по имени и вызов их методов;
В контексте Condition Expression Camunda неявно предоставляет переменную execution, а в ней есть, например, BusinessKey и ProcessDefinitionKey;
В Spring можно использовать аннотации @Component, @Autowired и @Value, что ну очень удобно.
Итого, чтобы поддержать выражения вида ${Claim.type (execution) == «emergency»} нам нужен примерно такой pom.xml и например такой Java-код:
pom.xml:
4.0.0
ru.fixprice.smeta
camunda
1.0.0-SNAPSHOT
org.springframework.boot
3.2.4
spring-boot-starter-parent
UTF-8
7.21.0
17
org.camunda.bpm
camunda-bom
${camunda.spring-boot.version}
import
pom
org.camunda.bpm.springboot
camunda-bpm-spring-boot-starter-rest
org.camunda.bpm.springboot
camunda-bpm-spring-boot-starter-webapp
org.postgresql
postgresql
42.6.0
runtime
org.springframework.boot
spring-boot-starter-jdbc
org.apache.httpcomponents.client5
httpclient5
org.springframework.boot
spring-boot-maven-plugin
ru.fixprice.smeta.Application
repackage
REST клиент для запросов к бэку:
package ru.fixprice.smeta.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import java.net.InetSocketAddress;
@Component
public class RestClientConfig {
@Value("${backend.api.token}")
private String token;
@Value("${backend.api.http-host}")
private String host;
@Bean
RestClient restClient() {
return RestClient.builder().defaultHeaders(httpHeaders -> {
httpHeaders.setHost(InetSocketAddress.createUnresolved(host, 0));
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
httpHeaders.set("token", token);
}).build();
}
}
Базовый класс для доступа к сущностям разных типов:
package ru.fixprice.smeta.decision;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.client.RestClient;
abstract public class AbstractDecisionWrapper {
@Value("${backend.api.base-url}")
private String baseUri;
@Autowired
protected RestClient restClient;
protected RestClient.ResponseSpec send(DelegateExecution execution, String path) {
String moduleUri = "/api/v1/camunda/";
return restClient.get().uri(baseUri + moduleUri + path, execution.getBusinessKey()).retrieve();
}
}
Класс компонента для доступа к Claim (заявки):
package ru.fixprice.smeta.decision;
import jakarta.inject.Named;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component;
import ru.fixprice.smeta.responses.claim.ClaimTypeResponse;
import java.util.Objects;
@Component
@Named("Claim")
public class Claim extends AbstractDecisionWrapper {
public String type(DelegateExecution execution) {
var result = send(execution, "claim/type/{id}").body(ClaimTypeResponse.class);
return Objects.requireNonNull(result).data().type();
}
}
Класс респонса с типом заявки:
package ru.fixprice.smeta.responses.claim;
public record ClaimTypeResponse(Data data) {
public record Data(String type) {
}
}
Отдельно на стороне бэкенда для Camunda создается контроллер, который возвращает данные в нужном формате.
О проблемах
Обратные запросы из Camunda сломали API-тесты в Codeception. Причина в том, что эти тесты исполняются без запуска веб-сервера, и каждый тест оборачивается в транзакцию. В конце теста транзакция откатывается, и БД получает первозданный вид. Это работает быстро и может быть распараллелено. Однако Camunda обращается к бэкенду через веб-сервер и не видит данные, завернутые в транзакции!
В попытках спасти положение отключили транзакции и переключились на раскатывание базы начисто перед каждым тестом — это увеличило время их исполнения с 5 минут до получаса. Сошлись на том, что API-тесты не должны задействовать Camunda, так как это уже интеграция. В апи-тестах эффекты от Camunda мокируются.
Взгляд со стороны аналитика
Внедряли мы эту систему для сервиса работы с заявками по магазинам. BPM-движок, такой как Camunda, предназначен для автоматизации и оптимизации бизнес-процессов. Он помогает моделировать, реализовывать и управлять процессами, обеспечивая прозрачность и контроль на всех этапах. Вот несколько ключевых причин, почему BPM-движки становятся неотъемлемой частью современных бизнес-систем:
Упрощение процессов: BPM-движок позволяет визуализировать и структурировать сложные процессы, что делает их более понятными для всех участников.
Автоматизация задач: С помощью BPM-движка можно автоматизировать рутинные задачи и этапы, что снижает вероятность ошибок и освобождает сотрудников для более творческой работы.
Мониторинг и аналитика: BPM-движки предоставляют инструменты для мониторинга процессов в реальном времени, что позволяет быстро реагировать на изменения и выявлять узкие места.
Гибкость и адаптивность: BPM-движки позволяют легко изменять и настраивать процессы в соответствии с изменениями в бизнес-среде, что критически важно в условиях динамичного рынка.
Camunda — это мощный BPM-движок, который поддерживает нотацию BPMN (Business Process Model and Notation) для моделирования процессов, CMMN (Case Management Model and Notation) для управления делами и DMN (Decision Model and Notation) для автоматизации бизнес-правил.
Опыт реализации
В начале реализации проекта с Camunda наша команда пыталась детализировать каждое действие на схеме бизнес-процесса. Однако, по мере продвижения, мы пришли к выводу, что нет смысла отображать всю логику на схеме, и оставили только важные части, влияющие на переходы состояний в процессе. Этот процесс потребовал четырех итераций оптимизации схемы бизнес-процесса и согласования подхода с программистом.
Использование инструмента Camunda Modeler сыграло ключевую роль в этом процессе. Camunda Modeler позволил нам визуализировать бизнес-процессы и говорить на одном языке с командой разработки. Это значительно повысило погруженность в детали продукта при его реализации, так как все участники процесса могли видеть изменения и обсуждать их в контексте общей схемы. Мы смогли упростить модель, сохранив при этом ключевые элементы, что сделало ее более понятной как для разработчиков, так и для бизнес-аналитиков. Итогом стало создание эффективной схемы, которая четко отражала бизнес-логику без излишней детализации.
Схема до оптимизации:
Схема после оптимизации:
Пример подпроцесса в оптимизированной схеме:
Пример реализации бизнес-правила для отображения кнопок в зависимости от роли в нотации DMN:
Внедрение Camunda как BPM-движка для сервиса работы с заявками по магазинам может значительно улучшить эффективность бизнес-процессов. Однако, как и любое решение, она имеет свои плюсы и минусы, и их необходимо учитывать при принятии решения о внедрении. И вот недостатки Camunda которые мы выявили:
Кривая обучения. Несмотря на интуитивный интерфейс, освоение всех возможностей Camunda может потребовать времени, особенно для пользователей без технического образования.
Сложности с настройкой. В зависимости от специфики бизнеса, настройка и интеграция Camunda могут быть сложными и потребовать значительных ресурсов.
Лицензирование. Хотя Camunda имеет бесплатную версию, некоторые функции доступны только в платной версии, что может быть ограничением для компаний.
Зависимость от IT. Для настройки и оптимизации процессов может потребоваться помощь IT-специалистов, что может быть дополнительной нагрузкой для компании.
А на этом у меня всё, не стесняйтесь спрашивать о важном в комментариях!