Разработка обработчика событий интеграции для утверждения платежа

Разработка обработчика событий интеграции для утверждения платежа

1. Введение

В среде микросервисной архитектуры (MSA) каждый доменный сервис имеет независимые бизнес-обязанности и собственную базу данных. Домен учета рабочего времени и отпусков (сервисы Timecard/Leave) в этой системе также функционирует в рамках собственного жизненного цикла. Однако ключевые бизнес-процессы, такие как подача пользователями заявок на отпуск (LeavePlan) и заявок на особый режим работы, требуют тесной интеграции с общекорпоративным доменом — внешним сервисом согласований. Изначально статусы согласования обрабатывались внутри сервиса, но стандартизация электронных согласований на уровне компании потребовала построения интеграционного конвейера с внешним сервисом согласований. Однако если взаимодействие с внешней системой согласований в среде MSA реализуется простым синхронным способом, задержки или сбои сервера согласований могут распространяться на внутренние сервисы и блокировать транзакции. Кроме того, поскольку согласование по цепочке (Approved), отклонение (Rejected) и отзыв (Withdrawn) происходят асинхронно после отправки пользователем черновика, потребовалась архитектура, гарантирующая согласованность данных между двумя сервисами в конечном итоге. В статье рассматривается практический вариант реализации, в котором во входящем слое timecard-facade был создан ApprovalStatusHandler, объединенный с SubmitLeaveRequestTask и LeavePlanTask для построения асинхронного конвейера синхронизации статусов согласования на основе событий в среде MSA.

2. Обоснование выбора технологии

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

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

  2. Сопоставление доменов на основе внешнего идентификатора (approvalId): идентификатор документа согласования (approvalId), создаваемый внешним сервисом согласований, связывался с внутренней сущностью заявки на отпуск (LeavePlan) в виде внешнего ключа (Reference Key). Это обеспечивало точный поиск целевого домена при последующем получении асинхронных событий изменения статуса.

  3. Идемпотентность событий и проверка переходов состояний: мы разработали защиту от повторной обработки на уровне сущности (State-based Guard), предотвращающую повреждение внутренних данных даже в случае многократного получения одного и того же события завершения согласования из-за повторной передачи по сети.

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

3. Процесс реализации и устранение неполадок

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

3.1. Запрос на отправку в сервис согласований и сопоставление идентификатора (SubmitLeaveRequestTask)

Когда пользователь отправляет заявку на отпуск, SubmitLeaveRequestTask проверяет, указан ли внешний идентификатор документа согласования (approvalId), и устанавливает исходный статус InApproval (согласование выполняется). После выполнения проверок (остатка доступных дней и пересечений периодов) approvalId сохраняется в сущности LeavePlan, что завершает создание однонаправленной структуры сопоставления, позволяющей отслеживать события с результатами согласования, поступающие позднее асинхронно.

@Service
@RequiredArgsConstructor
public class SubmitLeaveRequestTask {

    private final LeavePlanLogic leavePlanLogic;
    private final LeavePermitLogic leavePermitLogic;

    public String submitLeaveRequest(String citizenId, LocalDate startDate, LocalDate endDate,
                                     LeaveType leaveType, double leaveDays, String approvalId) {

        LeavePermit leavePermit = getLeavePermit(citizenId, startDate.getYear());
        leavePermit.planLeave(leaveDays);
        leavePermitLogic.modifyLeavePermit(leavePermit);

        LeavePlanCdo leavePlanCdo = LeavePlanCdo.builder()
                .citizenId(citizenId)
                .leaveType(leaveType)
                .planStatus(StringUtils.hasText(approvalId) ? PlanStatus.InApproval : PlanStatus.Planned)
                .approvalId(approvalId)
                .build();

        return leavePlanLogic.registerLeavePlan(leavePlanCdo);
    }
}

3.2. Получение событий изменения статуса согласования и их маршрутизация (ApprovalStatusHandler)

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

@Component
@RequiredArgsConstructor
public class ApprovalStatusHandler {

    private final LeavePlanTask leavePlanTask;

    public void handleApprovalStatusChanged(ApprovalStatusEventPayload payload) {
        String approvalId = payload.getDocumentId();
        ApprovalStatus status = payload.getStatus();

        switch (status) {
            case Approved -> leavePlanTask.approveLeavePlanByApprovalId(approvalId, payload.getApproverId());
            case Rejected -> leavePlanTask.rejectLeavePlanByApprovalId(approvalId, payload.getRejectReason());
            case Cancel -> leavePlanTask.cancelLeavePlanByApprovalId(approvalId);
            default -> {
                log.info("Approval transient state received: {}", status);
            }
        }
    }
}

3.3. Проверка идемпотентности домена и обновление статуса (LeavePlanTask)

LeavePlanTask выполняет поиск сущности внутреннего домена по внешнему идентификатору согласования (approvalId). Затем он применяет защиту от повторной обработки (State-based Guard), предварительно проверяя текущий статус сущности (PlanStatus.Approved). Благодаря этому система не дает сбоя даже в случае повторного получения уже обработанного события согласования из-за повторных сетевых запросов или доставки дублирующего сообщения. Только допустимые переходы приводят к изменению статуса и сохранению метаданных согласования, после чего публикуется финальное доменное событие о завершении согласования, безопасно запускающее последующие процессы.

@Component
@RequiredArgsConstructor
public class LeavePlanTask {

    private final LeavePlanStore leavePlanStore;
    private final EventPublisher eventPublisher;

    @Transactional
    public void approveLeavePlanByApprovalId(String approvalId, String approverId) {
        LeavePlan leavePlan = leavePlanStore.findByApprovalId(approvalId)
                .orElseThrow(() -> new NoSuchElementException("연동된 휴가 건이 없습니다: " + approvalId));

        if (leavePlan.getPlanStatus() == PlanStatus.Approved) {
            return;
        }

        leavePlan.setPlanStatus(PlanStatus.Approved);
        leavePlan.setApproverId(approverId);
        leavePlan.setApprovedTime(System.currentTimeMillis());
        leavePlanStore.update(leavePlan);

        eventPublisher.publish(new LeavePlanEvent(leavePlan.getId(),     leavePlan.getRequesterId(), PlanStatus.Approved));
    }
}

3.4. Устранение неполадок: устранение несоответствий между изменениями статуса согласования и ограничениями проверки БД

На ранних этапах интеграции с сервисом согласований произошло нарушение ограничения CHECK на уровне БД (plan_status IN (...)) при переходе статуса внутреннего домена после отправки пользователем запроса на согласование. В существующей схеме внутреннего автомата состояний были определены только значения статуса, основанные на собственном процессе согласования сервиса, поэтому подробный статус жизненного цикла (InApproval), добавленный для интеграции с внешним сервисом согласований, отсутствовал. Для решения проблемы мы применили скрипт миграции Flyway, синхронизировав ограничения базы данных с актуальным жизненным циклом согласования, а также добавили в LeavePlanTask защиту отката при состоянии исключения, чтобы обеспечить согласованность данных во время выполнения.

4. Результаты и достижения

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

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

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

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

5. Заключение

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

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

* Ссылки

  • Enterprise Integration Patterns — обработчик, управляемый событиями, и диспетчер сообщений

  • Chris Richardson — Microservices Patterns (Saga и асинхронный обмен сообщениями)

  • Официальная документация Spring Framework — интеграция и обработчики событий

D.Hyeok

Site footer