Привет, меня зовут Комаров Алексей, я Java-разработчик в Норникель Спутник. Занимаюсь развитием и поддержкой системы АСУ НСИ (Автоматизированная система управления нормативно-справочной информацией).
В этой статье я хотел бы поделиться успешным опытом создания кастомного веб-приложения для ведения мастер-данных справочника УЕР и его запуска с использованием санкционно-независимого ПО.
Сценарий работы в системе
В АСУ НСИ ведутся и хранятся мастер-данные: материалы, контрагенты, работы и услуги, проекты и так далее. Реализован классический сценарий работы системы по ведению мастер-данных.
Логика работы пользователей следующая:
Пользователь заходит в систему
Ищет нужную запись
Если не нашел, то формирует запрос на создание.
Если нашел, то формирует запрос на изменение/выгрузку/блокировку
Заполняет все обязательные поля, проходит валидации
Отправляет на согласование
Ответственные пользователи согласовывают запрос
Каждый справочник имеет свой список согласующих и их последовательность. Запись проходит все шаги запроса, в финале ей присваивается уникальный идентификатор и запись выгружается в локальные системы. При этом каждая локальная система при получении записи возвращает ответ:
В случае успеха – локальный идентификатор записи
В случае ошибки при получении – код и текст ошибки
АСУ НСИ состоит из двух компонентов:
SAP Master Data Management (SAP MDM) – управление данными
SAP Enterprise Portal (SAP EP) – веб-портал, через который осуществляется работа пользователей с мастер-данными.
Ранее было принято решение о создании справочника и настройки цепочек согласования посредством Java веб-приложения в SAP EP, базой данных выступал SQL Server.
Удалось успешно реализовать гибкую и настраиваемую цепочку согласования. Появилась возможность внедрения любой бизнес-логики на любом шаге цепочки согласования в любом направлении, в том числе:
новая логика обработки данных (для примера, если цепочка согласования запроса долго ходит по кругу, то на пятый круг отправить почтовое уведомление руководителям)
сохранение данных для отчетности (история изменений записи на шагах запроса)
возможна вставка использования интеграции (отправка/получение данных смежных систем) на любых шагах согласования
рассылка почтовых уведомлений при выполнении настраиваемых
условий в бизнес-логике
Ранее было принято решение провести оценку санкционно-независимых решений в области НСИ. Одним из решений может являться контейнеризация веб-приложения на санкционно-независимых компонентах.
Наш новый справочник как раз реализован на Java и работает с реляционной БД.
Мы понимали, что реализованный справочник в SAP EP представляет из себя веб-приложение на Java, которое может быть легко запущено с использованием санкционно-независимого ПО, поэтому решили реализовать это на практике:
Вынести Java-код из SAP EP
Доработать Java-код до полноценного Spring Boot Application, заменив библиотеки SAP по работе с пользователями и полномочиями на Spring Security
Перенести структуру БД в PostgreSQL и запустить в контейнере
Сделать образ Java приложения с использованием отечественного образа Red OS и Liberia JDK
Запустить!
Вдохновились и сделали!
Проведенное функциональное тестирование подтвердило успех выполненной разработки.
Детализируем выполненную разработку
Приложение содержит две составляющие: Пользовательская и Техническая.
Со стороны пользователя
Разделы приложения имеют следующие функции:
Раздел «Записи»
поиск записей
просмотр результатов поиска
просмотр карточки каждой отдельной записи, который включает: все поля записи, история изменения записи на каждом шаге каждого типа запроса
персонализация интерфейса пользователя
создание запросов на создание, изменение, блокирование, копирование, выгрузка в смежные системы записи
экспорт записей в MS Excel
Раздел «Запросы»
поиск по полям записи и запросов
просмотр результатов поиска
-
просмотр карточки каждого запроса, который включает:
все поля записи
история изменения записи на каждом шаге запроса
обработка запросов на создание, изменение, блокирование, копирование, выгрузку в локальную систему записи
экспорт запросов в MS Excel
валидации записи на шагах согласования
персонализация интерфейса пользователя
С технической стороны (много Java-кода)
Раздел «Записи»
Главной задачей данного раздела является поиск и просмотр записей. Раздел реализован следующим образом: REST Controller принимает GET-запрос и входные параметры в Map<String, String> и передает их на вход Service:
@RestController
@RequestMapping(value = "/api/v1/uer")
public class UerController {
@ResponseStatus(HttpStatus.OK)
@GetMapping
public FindUerRecordsResponse find(@RequestParam Map<String, String> map) {
FindUerRecordsResponse response = new FindUerRecordsResponse();
List<UerDto> list = uerService.find(map);
response.setRecords(list);
response.setRecordsCount(list.size());
return response;
}
}
Service получает параметры и выполняет поиск с помощью Specification в JPA Repository:
@Service
@RequiredArgsConstructor
public class UerService {
@Transactional
public List<UerDto> find(Map<String, String> map) {
Specification<Uer> uerSpecification =
Specification.where(UerSpec.equalGid(map.get("gid")))
.and(UerSpec.likeCode(map.get("code")))
.and(UerSpec.likeUerName(map.get("uerName")))
.and(UerSpec.equalUerGroup(map.get("group")))
.and(UerSpec.equalUerSubgroup(map.get("subgroup")))
.and(UerSpec.likeSection(map.get("section")))
.and(UerSpec.equalWorkType(map.get("workType")))
.and(UerSpec.equalMu(map.get("mu")))
.and(UerSpec.likeProjectPin(map.get("projectPin")))
.and(UerSpec.likeProjectCode(map.get("projectCode")))
.and(UerSpec.equalRecordStatus(map.get("recordStatus")))
.and(UerSpec.equalCuratorApproveType(map.get("curatorApproveType")))
.and(UerSpec.equalMethodologApproveType(map.get("methodologApproveType")));
Pageable page = PageRequest.of(0, 1000, Sort.by(Sort.Direction.DESC, "creationDate"));
List<Uer> list = uerRepository.findAll(uerSpecification, page).toList();
return list.stream().map(o -> uerMapper.toUerDto(o)).collect(Collectors.toList());
}
}
public class UerSpec {
public static Specification<Uer> equalCuratorApproveType(String value) {
return StringUtils.hasText(value) ? ((root, query, criteriaBuilder) -> {
return criteriaBuilder.equal(root.get("curatorApproveType"), Boolean.parseBoolean(value));
}) : null;
}
public static Specification<Uer> equalGid(String gid) {
return StringUtils.hasText(gid) ? ((root, query, criteriaBuilder) -> {
return criteriaBuilder.equal(root.get("gid"), gid);
}) : null;
}
public static Specification<Uer> equalMethodologApproveType(String value) {
return StringUtils.hasText(value) ? ((root, query, criteriaBuilder) -> {
return criteriaBuilder.equal(root.get("methodologApproveType"), Boolean.parseBoolean(value));
}) : null;
}
public static Specification<Uer> equalMu(String value) {
return StringUtils.hasText(value) ? ((root, query, criteriaBuilder) -> {
Join<UerClassifier, Uer> g = root.join("mu");
return criteriaBuilder.equal(g.get("symbol"), value);
}) : null;
}
public static Specification<Uer> equalRecordStatus(String value) {
return StringUtils.hasText(value) ? ((root, query, criteriaBuilder) -> {
return criteriaBuilder.equal(root.get("recordStatus"), new RecordStatus(Integer.parseInt(value)));
}) : null;
}
public static Specification<Uer> equalUerGroup(String value) {
return StringUtils.hasText(value) ? ((root, query, criteriaBuilder) -> {
Join<UerClassifier, Uer> g = root.join("uerGroup");
return criteriaBuilder.equal(g.get("groupCode"), value);
}) : null;
}
public static Specification<Uer> equalUerSubgroup(String value) {
return StringUtils.hasText(value) ? ((root, query, criteriaBuilder) -> {
Join<UerClassifier, Uer> g = root.join("uerSubgroup");
return criteriaBuilder.equal(g.get("groupCode"), value);
}) : null;
}
public static Specification<Uer> equalWorkType(String value) {
return StringUtils.hasText(value) ? ((root, query, criteriaBuilder) -> {
WorkType o = new WorkType();
o.setId(Long.parseLong(value));
return criteriaBuilder.equal(root.get("workType"), o);
}) : null;
}
public static Specification<Uer> isActual(Integer value) {
return value == null ? null : ((root, query, criteriaBuilder) -> {
return criteriaBuilder.equal(root.get("isActual"), value);
});
}
public static Specification<Uer> likeCode(String value) {
return StringUtils.hasText(value) ? ((root, query, criteriaBuilder) -> {
return criteriaBuilder.like(root.get("code"), contains(value));
}) : null;
}
public static Specification<Uer> likeProjectCode(String value) {
return StringUtils.hasText(value) ? ((root, query, criteriaBuilder) -> {
return criteriaBuilder.like(root.get("projectCode"), value);
}) : null;
}
public static Specification<Uer> likeProjectPin(String value) {
return StringUtils.hasText(value) ? ((root, query, criteriaBuilder) -> {
return criteriaBuilder.like(root.get("projectPin"), value);
}) : null;
}
public static Specification<Uer> likeSection(String value) {
return StringUtils.hasText(value) ? ((root, query, criteriaBuilder) -> {
return criteriaBuilder.like(root.get("section"), contains(value));
}) : null;
}
public static Specification<Uer> likeUerName(String value) {
return StringUtils.hasText(value) ? ((root, query, criteriaBuilder) -> {
return criteriaBuilder.like(root.get("uerName"), contains(value));
}) : null;
}
public static Specification<Uer> notEqualRecordStatus(String value) {
return StringUtils.hasText(value) ? (root, query, criteriaBuilder) -> {
Join<RecordStatus, Uer> t = root.join("recordStatus");
return criteriaBuilder.notEqual(t.get("gid"), value);
} : null;
}
private static String contains(String expression) {
return MessageFormat.format("%{0}%", expression);
}
}
Поиск в БД выполняется с помощью Spring JPA Repository:
public interface UerRepository extends JpaRepository<Uer, Long>, JpaSpecificationExecutor<Uer> {
}
Раздел «Запросы»
Архитектура и логика работы раздела «Запросы» в части поиска и просмотра результатов запросов полностью аналогична архитектуре раздела «Записи».
Для создания запроса вызывается метод «create» класса RequestController, обрабатывающий POST запросы. На вход получаем RequestDto, в котором указывается тип необходимого запроса — создание, изменение, блокирование, копирование (дополнительно указывается из какой записи необходимо сделать копирование, взяв ее за основу).
Когда пользователь создал запрос и заполнил все необходимые поля, он нажимает одну из управляющих кнопок - сохранить, согласовать, отменить, отправить на уточнение или отправить на согласование. В этом случае выполняется PUT-запрос и запускается метод «update» класса RequestController, обрабатывающий PUT запросы. На вход получаем объект RequestDto со всеми данными, заполненными пользователем, action – кнопка, нажатая пользователем. Контроллер передает все полученные данные в класс Service, выполняющий всю бизнес-логику:
@RestController
@RequestMapping(value = "/api/v1/requests")
public class RequestController {
...
@ResponseStatus(HttpStatus.OK)
@PostMapping
public RequestDto create(@RequestBody RequestDto requestDto) {
return requestService.create(requestDto);
}
@ResponseStatus(HttpStatus.OK)
@PutMapping
public RequestDto update(@RequestBody RequestDto requestDto) {
return requestService.update(requestDto);
}
}
Класс Service выполняет часть бизнес-логики и передачу обработки классу BusinessCommand, который выполнит всю необходимую бизнес-логику для перехода из текущего статуса запроса в необходимый, в соответствии с нажатой кнопкой (отправить на согласование, отправить на уточнение, отменить, согласовать и т. д.) - это «Теория автоматов» (у нас есть текущее состояние, мы получили команду на переход в новое состояние, выбираем нужную бизнес-команду и выполняем надлежащую бизнес-логику). Код:
@Service
@RequiredArgsConstructor
public class RequestService {
…
private final BusinessCommandFactory factory;
@Transactional
public RequestDto create(RequestDto in) {
//в полученном объекте Dto приходит требуемый тип запроса:
//создание, изменение, блокирование, копирование.
//Преобразуем маппером в объект Entity
Request request = requestMapper.toRequestEntity(in);
//получаем класс-обработчик статуса черновик - 011
BusinessCommand businessCommand = factory.buildProcessor(RS.S011);
//обработчик businessCommand создаст запрос, запись,
//заполнит все необходимые поля начальными
//значениями и сделает любую нужную бинес-логику
businessCommand.execute(request);
// отдаем результат выполнения на фронтентд
return requestMapper.toRequestDto(((S011Draft) buildProcessor).getReq());
}
@Transactional
public RequestDto update(RequestDto requestDto) {
// запускаем валидации
validator.execute(requestDto);
// при успехе валидаций сохраняем полученные данные, что бы при форс-мажоре
// пользователю не нужно было заново вводить данные
uerService.save(requestDto);
Request request = requestRepository.findById(requestDto.getId()).orElseThrow();
// сохраняем состояние сущности запроса (в ней же и сама запись) в историю
// изменений на данном шаге
helper.saveToHistory(request);
// вычисляем следующий статус по текущему статусу запроса и выбранному action,
// на вход передаем запрос, так как при вычислении конечного статуса могут
// понадобится дополнительные данные из данных запроса
String targetStatus = BusinessRules.getStatus(request);
// получаем обработчика, который выполнит всю бинзес логику для перехода на
// следующий статус
BusinessCommand businessCommand = factory.buildProcessor(targetStatus);
// запускаем обработку
businessCommand.execute(requestRepository.findById(requestDto.getId()).orElseThrow());
// отдаем результат выполнения на фронтентд
return requestMapper.toRequestDto(requestRepository.findById(requestDto.getId()).orElseThrow());
}
}
Класс BusinessCommandFactory — фабрика создающая классы BusinessCommand, выполняющие всю бизнес-логику при переходе на необходимый статус запроса. Класс содержит объект Map с ключом (статус запроса) и value (объект класса бизнес-команды). При вызове класса BusinessCommandFactory достаем из контекста Spring нужный бин бизнес-команды. Внутри бизнес-команды может быть встроена любая бизнес-логика.
@Component
@RequiredArgsConstructor
public class BusinessCommandFactory {
private final static Map<String, Class> map = new HashMap<>();
static {
map.put(RS.S011, S011Draft.class);
map.put(RS.S012, S012ReviewByInitiatorFromExpert.class);
map.put(RS.S013, S013ReviewByInitiatorFromCurator.class);
map.put(RS.S020, S020SendToExpert.class);
map.put(RS.S022, S022SendToExpertFromInitiatorReview.class);
map.put(RS.S023, S023ReviewByExpertFromCurator.class);
map.put(RS.S024, S024ReviewByExpertFromMethodolog.class);
map.put(RS.S030, S030SendToCurator.class);
map.put(RS.S033, S033SendToCuratorFromExpertReview.class);
map.put(RS.S034, S034ReviewByCuratorFromMethodolog.class);
map.put(RS.S040, S040SendToMethodolog.class);
map.put(RS.S044, S044SendToMethodologFromCuratorReview.class);
map.put(RS.S120, S120SendToRS.class);
map.put(RS.S140, S140Cancel.class);
}
private final ApplicationContext context;
public BusinessCommand buildProcessor(String status) {
Class<?> c = map.get(status);
BusinessCommand bean = (BusinessCommand) context.getBean(c);
return bean;
}
}
Рассмотрим в качестве примера переход на шаг согласования после заполнения пользователем черновика запроса:
@Component
public class S020SendToExpert extends BusinessCommand {
...
public void execute(Request r) {
//заполним даты рассылки электронных писем пользователям
//о необходимости обработать запрос
deadlineService.fillNotifications(r, r.getRequestType().getCode(), Step.METHODOLOG);
helper.addToRequestHistory(r, RS.S020);
r.setRequestStatus(helper.getRequestStatus(RS.S020));
r.setUserComment(null);
String targetStatus = RS.S020;
//заполняем пользователей - потенциальных обработчиков на данном шаге
//в соответствии с ролевой моделью
List<PotencialOwner> list = new ArrayList<>();
String[] roles = BusinessRules.getPotencialOwnerRole(targetStatus);
for (int i = 0; i < roles.length; i++) {
String string = roles[i];
List<UerUser> userList = helper.getRoleMembers(string);
Iterator<UerUser> iterator = userList.iterator();
while (iterator.hasNext()) {
UerUser uerUser = iterator.next();
PotencialOwner potencialOwner = new PotencialOwner();
potencialOwner.setRequest_id(r);
potencialOwner.setUser_id(uerUser);
list.add(potencialOwner);
}
}
r.getPotencialOwners().clear();
r.getPotencialOwners().addAll(list);
// очищаем обработчика на предыдущем шаге
r.getRequestProcessor().clear();
r = requestRepository.save(r);
//отправляем уведомление о новом запросе всем ответственным за обработку
sendEmail(r);
}
private void sendEmail(Request request) {
List<String> roleMembersEmails = helper.getRoleMembersEmails(Roles.EXPERT);
String subject = String.format("Запрос %s отправлен на согласование эксперту", request.getId());
String url = "test_host";
String body = String.format(
"Запрос <a href='%s' target='_blank'>%s</a> на %s записи %s, %s отправлен на согласование.", url,
request.getId(), request.getRequestType().getDescr(), request.getUer().getGid(),
request.getUer().getUerName());
sendEmail(request, roleMembersEmails, subject, body);
}
}
Все бизнес-команды наследуются от абстрактного класса BusinessCommand, который содержит бины для выполнения бизнес-логики и вспомогательные методы для отправки почтовых уведомлений. Таким образом не требуется каждый раз в новой команде прописывать бины заново чтобы их использовать. Такая архитектура позволяет получить веб-приложение, готовое к любому расширению бизнес-логики в любом необходимом направлении.
@Component
public abstract class BusinessCommand {
...
@Autowired
protected Helper helper;
@Autowired
protected RequestMapper requestMapper;
@Autowired
protected EmailRepository emailRepository;
@Autowired
protected DeadlineService deadlineService;
@Autowired
protected RequestRepository requestRepository;
@Autowired
protected UerRepository uerRepository;
@Autowired
protected RequestTypeRepository requestTypeRepository;
@Autowired
protected RequestStatusRepository requestStatusRepository;
@Autowired
protected RecordStatusRepository recordStatusRepository;
public abstract void execute(Request request);
protected void sendEmail(Request request, List<String> emails, String subject, String bodyS) {
StringBuilder body = new StringBuilder();
body.append(MailHelper.Texts.HEADER);
body.append(bodyS);
body.append(MailHelper.Texts.FOOTER);
MailHelper.sendEmail(emails, subject, body.toString(), MailHelper.ContentType.HTML);
Email email = new Email();
email.setRequestId(request.getId());
email.setRequestStatus(request.getRequestStatus().getCode());
email.setEmailFrom(Helper.mailFrom());
email.setEmailTo(String.join(", ", emails));
email.setSubject(subject);
email.setBody(body.toString());
email.setCreationDate(new Timestamp(System.currentTimeMillis()));
emailRepository.save(email);
}
protected void sendEmailToInitiator(Request r) {
List<String> email = helper.getCurrentUserEmail();
String subject = String.format("Эксперт изменил данные запроса %s внесенные инициатором", r.getId());
String host = "test_host";
String url = host + "/index.html#/task/" + r.getId();
String body = String.format(
"Эксперт изменил данные запроса <a href='" + url
+ "' target=\"_blank\">%s</a> на %s УЕР, %s внесенные инициатором",
r.getId(), r.getRequestType().getDescr(), r.getUer().getUerName());
sendEmail(r, email, subject, body);
}
}
История изменений записи на шагах запроса реализована в классе RequestService. При переходе на новый шаг мы сохраняем состояние записи на текущий момент, чтобы пользователь смог увидеть, как менялась запись от шага к шагу в цепочке согласования. С помощью библиотеки Jackson мы конвертируем объект в json-строку и складываем ее в отдельное поле в PostgreSQL.
@Component
@RequiredArgsConstructor
public class Helper {
public void saveToHistory(Request request) {
ObjectMapper objectMapper = new ObjectMapper();
String json;
RequestDto requestDto = requestMapper.toRequestDto(request);
json = objectMapper.writeValueAsString(requestDto);
Uer u = request.getUer();
historyRepository.save(new UerHistory(u.getSodRecId(), u.getGid(), request.getId(), request.getRequestStatus().getCode(), json, "request", u.getSodRecId()));
}
}
Далее фронтенд запрашивает данные для определенной записи, получает список json-строк и передает их на вход объекту таблицы:
public List<String> findUerHistory(Integer id) {
return historyRepository.findByUerIdOrderByIdAsc(id).stream().map(o -> o.getJson()).collect(Collectors.toList());
}
Запуск приложения (CI/CD)
В качестве основы для запуска используется Docker-образ Red OS с предварительно установленной Liberica JDK 17. Последовательность работы pipe:
создаем jar файл нашего веб-приложения
упаковываем его в образ Red OS
запускаем с помощью Liberica JDK
сохраняем образ в Nexus
запускаем на удаленной машине образ приложения и PostgreSQL с
помощью docker-compose файла
Файл .gitlab-ci.yml
stages:
- build-jar
- build-image
- deploy-image
build-jar:
stage: build-jar
image: ${BUILD_IMAGE}
variables:
TRUSTED_ROOT_CERT_PATH: /tmp/cert.pem
script:
- apt-get update && apt-get --assume-yes install wget gnupg && wget -q -O - https://download.bell-sw.com/pki/GPG-KEY-bellsoft | apt-key add -
- echo "deb [arch=amd64] https://apt.bell-sw.com/ stable main" | tee /etc/apt/sources.list.d/bellsoft.list && apt-get update && apt-get --assume-yes install bellsoft-java17
- mvn clean package
artifacts:
paths:
- target/*.jar
build-image:
stage: build-image
dependencies:
- build-jar
script:
- docker login -u ${MAVEN_USER} -p ${MAVEN_PASS} nexus.host
- docker build -t nexus.host/uer-redos:1.0.1 .
- docker push nexus.host/uer-redos:1.0.1
.deploy-image:
stage: deploy-image
script:
- ssh -p 9000 -o "StrictHostKeyChecking=no" ${TARGET_USER}:${TARGET_PASSWORD}@${TARGET_MACHINE} 'cd /uer; docker-compose down; docker-compose up -d'
Файл docker-compose
version: '3'
services:
back:
image: back
environment:
- SPRING_PROFILES_ACTIVE=dev
- spring.datasource.url=jdbc:postgresql://postgres:5432/postgres
ports:
- "9889:9889"
postgres:
image: postgres:14.7
domainname: postgres
ports:
- "5432:5432"
volumes:
- v-postgres:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD", "pg_isready", "-q", "-U", "postgres"]
interval: 5s
timeout: 1s
retries: 2
Выводы
Приложив не такие большие усилия, мы получили собственное полноценное рабочее веб-приложение, которое:
-
имеет возможность внедрения любой бизнес-логики на любом шаге цепочки согласования в любом направлении, в том числе:
новая логика обработки данных (для примера, если цепочка согласования запроса долго ходит по кругу, то на пятый круг отправить почтовое уведомление руководителям)
сохранение данных для отчетности (история изменений записи на шагах запроса)
возможна вставка использования интеграции (отправка/получение данных смежных систем) на любых шагах согласования
рассылка почтовых уведомлений при выполнении настраиваемых условий в бизнес-логике
работает на санкционно-независимых компонентах
мы прошли функциональное тестирование, что подтверждает работоспособность
Легко тиражируется - тиражирование данного решения легко может быть выполнено для других справочников и другой структуры данных, так как вся архитектура останется точно такой же - меняется только ключевая сущность Uer. Сущность Request остается всегда одной и той же для всех видов справочников, при необходимости можно и ее дополнить нужными полями, на работе приложения это никак не скажется.
Комментарии (7)
aleksandy
20.12.2023 10:05map.get("gid")
Параметры в какую-нибудь модельку завернуть не думали?
Почему бы вот эту и подобную трешатину
StringUtils.hasText(gid) ? UerSpec.equalGid(gid) : null
внутри соответствующих методов
UerSpec
не делать?root.get("projectCode")
Ну, есть же нормальные инструменты, зачем ломаться в рантайме, если можно при компиляции?
MessageFormat.format("%{0}%", expression)
Вот это зачем? Тут нет никакого форматирования параметров. Конкатенации вполне достаточно.
работает на санкционно-независимых компонентах
С чего это? Liberica JDK продукт иностранной компании BellSoft. @olegchir не даст соврать.
ALexKud
20.12.2023 10:05C java есть интеграция у той же opensource bpm Camunda, к примеру. Изобретать свой велосипед конечно лучше, но долго и затратно. Я тоже создаю иногда велосипеды бизнес процессов, ибо компания где работаю автоматизирована слабо, разработчиков мало, а хочется быть как люди. Правда web не нужен, так как корпоративный vpn все покрывает локалкой. Да и задачи в основном связаны с "железом" интеллектуальных приборов и web разработка в данном контексте нужна как козе баян. К примеру на автоматизацию процесса ремонта приборов ушло примерно 6 месяцев и один разработчик и это при наличии не очень чёткого тз с интеграцией с 1с, с мониторингом пользователей, логов и разработки внутрипрограммного мессенджера на 60 плльзователей.
kochetkov-ma
20.12.2023 10:05Куча шаблонного кода, который можно сгенерировать в LLM...
Лучше найти какую-нибудь одну особенность системы и рассказать про нее - может кому-то будет интересно.
Ещё форматировать весь код в примерах в едином стиле, разные отступы выглядят как минутные стрелки с отличными углами на стене с часами по таймзонам городов...
И имя публичного класса Helper намекает на проблемы с неймингом...
Ещё образ собирается с тегом latest.. Если уж пишите сами пайплайны, то попросите девопсов сделать ревью перед публикацией
EvgeniyJVM
20.12.2023 10:05Так как все бизнес-команды наследуются от BusinessCommand, то в BusinessCommandFactory мапу можно объявить так:
private final Map<String, BusinessCommand> map;
И вручную ее можно не заполнять, спринг сам заполнит ее бинами наследниками BusinessCommand
Получится в итоге так
@Component @RequiredArgsConstructor public class BusinessCommandFactory { private final Map<String, BusinessCommand> map; public BusinessCommand buildProcessor(String status) { return map.get(status); } }
olegchir
20.12.2023 10:05Привет! Liberica JDK - это не отечественный дистрибутив Java. Начиная с весны прошлого года, Либерику делает американская компания BELLSOFT, и без VPN даже нельзя открыть экран загрузки.
Отечественный дистрибутив называется Axiom JDK. Часть разработчиков Аксиомы — это те же люди, что когда-то делали Либерику.
Slipeer
Если бы вы взяли готовый BPM Engine и сконцентрировались на создании интерфейсов для пользователей, необходимых интеграций и автоматизации отдельных шагов процесса (где требуется) - разве вы бы не получили бы результат быстрее, легче и более гибкий?
Задача автоматизации бизнес процессов достаточно типовая и написаны миллионы строк готового кода, её автоматизующего. Причем довольно гибко.