Общая информация
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-файлом в каталоге |
Динамический код |
Java-исходники из каталога |
События и автоматизации |
Реакция на изменение процесса, параметра или другой сущности без изменения вызывающего 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.
Куда смотреть дальше
-
База данных — модель хранения и доступ к данным;
-
Система кешей — локальные снимки, сброс, многопоточность и синхронизация инстансов;
-
Архитектурные тесты — ограничения пакетов и зависимостей;
-
Асинхронная работа с внешними сервисами — границы транзакций, повтор и уведомления;
-
Телеметрия — метрики и трассировка;