Общая информация

ERP — модульный монолит на Java и Kotlin. Приложение запускает встроенный Apache Tomcat и использует Servlet API, Apache Struts 1.3, JSP и Vue 2. Основная бизнес-логика, веб-слой и инфраструктура находятся в модуле core; модуль app собирает дистрибутив и содержит точку запуска, а frontend хранит общие веб-ресурсы.

Собранное приложение представляет собой один процесс JVM. При запуске ru.bgcrm.app.MainKt передаёт управление ru.bgcrm.Server, который:

  • настраивает и запускает embedded Tomcat;

  • проверяет подключение к основной БД;

  • запускает шину сообщений и загружает плагины;

  • регистрирует системные обработчики событий;

  • компилирует динамический Java-код;

  • запускает планировщики, JMX, gRPC и остальные подсистемы.

Обработка HTTP-запроса

Фильтры и маршрутизация

Перед сервлетом запрос проходит через фильтры из WEB-INF/web.xml. Они устанавливают UTF-8, восстанавливают пользователя из сессии, подготавливают параметры запроса, добавляют телеметрию и обработку ошибок.

Запросы с окончанием .do принимает ru.bgcrm.servlet.ActionServlet — расширение Struts ActionServlet. При инициализации он загружает все файлы WEB-INF/struts-config*.xml, поэтому свои mappings могут добавлять как ядро, так и подключённые модули.

Типичная конфигурация action:

<action path="/login"
        parameter="action"
        type="ru.bgcrm.struts.action.LoginAction"
        name="form"
        scope="request">
    <forward name="default" path="/login.jsp"/>
</action>

Параметр action выбирает метод класса action. Например, запрос /user/process.do?action=process вызывает метод process; если параметр не передан, вызывается unspecified. HTTP-метод не участвует в выборе Java-метода: один action может принять как GET, так и POST, если прикладной код не ограничивает это отдельно.

Большинство action наследуются от ru.bgcrm.struts.action.BaseAction. Доступные методы имеют одну из поддерживаемых сигнатур: получают ActionMapping, DynActionForm и Connection либо ConnectionSet, а при необходимости также HttpServletRequest и HttpServletResponse.

Форма, пользователь и права

ru.bgcrm.struts.form.DynActionForm объединяет входные параметры и состояние ответа. Через неё action получает:

  • параметры запроса;

  • текущего пользователя;

  • права на вызываемое действие;

  • пагинацию и загруженный файл;

  • HTTP request/response;

  • данные и клиентские события будущего JSON-ответа.

Перед вызовом метода BaseAction создаёт транзакционный ServerContext, привязывает его к текущему потоку и определяет право по ключу <полное имя Action>:<action>. Если право пользователю не выдано, вызов разрешается только для узла с allowAll=true.

Транзакция и завершение запроса

ServerContext хранит пользователя, лениво открываемые соединения, repositories, сервисы и callbacks после commit. Старый код получает соединения через переданный ConnectionSet; новый код должен обращаться к сервисам и repositories из контекста.

Успешное выполнение action завершается общим commit. После него выполняются callbacks runAfterCommit. При исключении commit не вызывается, контекст закрывается, а клиент получает доменную или техническую ошибку в зависимости от типа исключения.

Подробнее о контексте, DI и транзакциях:

Формирование ответа

Action может вернуть:

  • JSP forward;

  • JSON;

  • файл или другой потоковый ответ.

Методы processUserTypedForward(…​) выбирают между HTML и JSON. По умолчанию используется HTML; JSON запрашивается параметром responseType=json либо заголовком Accept: application/json. Параметры forward и forwardFile позволяют выбрать другой HTML forward, если action это поддерживает.

Стандартный JSON-ответ сериализуется из DynActionForm.response:

{
  "status": "ok",
  "message": "",
  "data": {},
  "eventList": []
}

data содержит прикладной результат, а eventList — команды для интерфейса. Для ожидаемой ошибки валидации используется статус invalid, для остальных ошибок — error; часть старого кода также использует message.

Завершившиеся вызовы, для которых не отключено журналирование в узле прав, записываются в помесячные таблицы web_requests_log_YYYYMM. В журнал попадают пользователь, action, параметры, длительность, результат и инстанс ERP. Записи доступны в оснастке «Администрирование → Web-запросы».

События и расширение поведения

Прикладные операции создают события, чтобы отделить основное изменение от дополнительной реакции на него. ru.bgcrm.event.EventProcessor последовательно вызывает подписчиков одного типа события, а затем передаёт событие в шину сообщений. Обработчики выполняются в контексте операции и могут участвовать в её транзакции, поэтому их ошибка способна отменить вызывающее действие.

Основные API:

  • ru.bgcrm.event.Event — базовый тип события;

  • ru.bgcrm.event.EventProcessor — регистрация и вызов обработчиков;

  • ru.bgcrm.event.listener.EventListener — контракт обработчика;

  • ru.bgcrm.event.listener.dispatcher.AnnotatedEventListener — декларативная диспетчеризация методов по аннотациям.

DynamicEventListener остаётся базой для динамических обработчиков. Такой listener запускается через внутренний executor, но вызывающий поток синхронно ожидает завершения. Таймаут задаётся @EventProcessingTimeout либо настройкой event.processTimeout; значение по умолчанию — 1000 мс. Это защита от зависшего динамического скрипта, а не способ выполнить работу в фоне.

Полный порядок вызова, варианты подписки, транзакционные границы и диагностика описаны в документации системы событий. Для новых динамических обработчиков с несколькими событиями используйте AnnotatedEventListener. Долгую работу после успешного commit передавайте в AsyncWorker, не создавая собственные потоки и executors.

Способы расширения

ERP расширяется несколькими механизмами:

Механизм Когда используется

Модуль Gradle

Компилируемая функциональность, которая включается в общий дистрибутив. Модуль может добавить Kotlin/Java-код, Struts mappings, веб-ресурсы и конфигурацию плагина.

Плагин ERP

Функциональность, описанная XML- или JSON-файлом в каталоге plugin. PluginManager создаёт класс плагина и собирает предоставленные им действия, WebSocket endpoints и классы инициализации.

Динамический код

Java-исходники из каталога dynamic.src.dir (устаревшее имя — dyn.src.dir, значение по умолчанию — dyn). DynamicClassManager компилирует их при запуске и при обновлении, а классы загружаются без пересборки основного приложения.

События и автоматизации

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

Новая платформенная функциональность размещается в компилируемых модулях. Клиентская специфика и существующие динамические обработчики остаются в dyn-коде. Общие точки расширения описаны в разделе «Расширение функционала».

Фронтенд

ERP не является отдельным SPA. Серверный JSP формирует страницу и монтирует в неё Vue-компоненты. Старые экраны используют JSP/JSTL, Struts taglib, jQuery и собственные taglib; новый и перерабатываемый интерфейс строится на Vue 2 и TypeScript.

Редактируемый общий фронтенд находится в frontend/src/main/resources/webapps. Модули могут поставлять собственные каталоги webapps; при сборке они объединяются в одно дерево приложения. Каталоги app/webapps, build, out, а также корневые js и ts являются копиями или устаревшими заготовками и не должны использоваться как исходники.

Компоненты

Однофайловые .vue-компоненты загружаются BGCRM.VueLoader и транспилируются в браузере. Отдельного production-bundle для приложения нет. JSP обычно создаёт уникальный контейнер через u:uiid(), чтобы несколько экземпляров одного виджета не конфликтовали:

<c:set var="uuid" value="${u:uiid()}"/>

<div id="${uuid}">
    <create-linked-process-widget/>
</div>

<script>
    window["${uuid}"] = new Vue({
        el: "#${uuid}",
        components: {
            "create-linked-process-widget":
                "url:/components/ui/process/CreateLinkedProcessWidget.vue"
        }
    });
</script>

Обращение к бэкенду

Фронтенд обращается к Struts actions через repositories на базе ApiGroup/axios-actions. Реализации находятся в webapps/js/thesaurus/repositories, а TypeScript-контракты — в webapps/ts/types/repositories.

Общий interceptor:

  • преобразует обычное тело запроса в FormData;

  • запрашивает JSON, если явно не выбран HTML;

  • передаёт элементы eventList в processEvent;

  • отклоняет Promise для ответов со статусом error, invalid или message.

Не вызывайте axios напрямую, если для операции можно добавить или расширить repository. Это сохраняет единый формат запросов, обработку ошибок и клиентских событий.

Клиентские события

Клиентские события наследуют ru.bgcrm.event.client.ClientEvent и передают UI-команды: уведомление, индикатор загрузки, открытие процесса и другие действия.

Внутри HTTP-запроса событие добавляется в form.response.eventList и приходит вместе с JSON. Фоновый код отправляет событие через Redis-очередь ClientEventNotification, которую активная вкладка забирает поллингом. Форматы, ограничения и готовые helpers описаны в документации ClientEvents.

Куда смотреть дальше