Привет, меня зовут Комаров Алексей, я 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)


  1. Slipeer
    20.12.2023 10:05

    Если бы вы взяли готовый BPM Engine и сконцентрировались на создании интерфейсов для пользователей, необходимых интеграций и автоматизации отдельных шагов процесса (где требуется) - разве вы бы не получили бы результат быстрее, легче и более гибкий?
    Задача автоматизации бизнес процессов достаточно типовая и написаны миллионы строк готового кода, её автоматизующего. Причем довольно гибко.


  1. aleksandy
    20.12.2023 10:05

    map.get("gid")

    Параметры в какую-нибудь модельку завернуть не думали?

    Почему бы вот эту и подобную трешатину

    StringUtils.hasText(gid) ? UerSpec.equalGid(gid) : null

    внутри соответствующих методов UerSpec не делать?

    root.get("projectCode")

    Ну, есть же нормальные инструменты, зачем ломаться в рантайме, если можно при компиляции?

    MessageFormat.format("%{0}%", expression)

    Вот это зачем? Тут нет никакого форматирования параметров. Конкатенации вполне достаточно.

    работает на санкционно-независимых компонентах

    С чего это? Liberica JDK продукт иностранной компании BellSoft. @olegchir не даст соврать.


  1. ALexKud
    20.12.2023 10:05

    C java есть интеграция у той же opensource bpm Camunda, к примеру. Изобретать свой велосипед конечно лучше, но долго и затратно. Я тоже создаю иногда велосипеды бизнес процессов, ибо компания где работаю автоматизирована слабо, разработчиков мало, а хочется быть как люди. Правда web не нужен, так как корпоративный vpn все покрывает локалкой. Да и задачи в основном связаны с "железом" интеллектуальных приборов и web разработка в данном контексте нужна как козе баян. К примеру на автоматизацию процесса ремонта приборов ушло примерно 6 месяцев и один разработчик и это при наличии не очень чёткого тз с интеграцией с 1с, с мониторингом пользователей, логов и разработки внутрипрограммного мессенджера на 60 плльзователей.


  1. LeshaRB
    20.12.2023 10:05

    А зачем в сервисе request Param и валидация?


  1. kochetkov-ma
    20.12.2023 10:05

    Куча шаблонного кода, который можно сгенерировать в LLM...
    Лучше найти какую-нибудь одну особенность системы и рассказать про нее - может кому-то будет интересно.
    Ещё форматировать весь код в примерах в едином стиле, разные отступы выглядят как минутные стрелки с отличными углами на стене с часами по таймзонам городов...
    И имя публичного класса Helper намекает на проблемы с неймингом...
    Ещё образ собирается с тегом latest.. Если уж пишите сами пайплайны, то попросите девопсов сделать ревью перед публикацией


  1. 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);
    	}
    }


  1. olegchir
    20.12.2023 10:05

    Привет! Liberica JDK - это не отечественный дистрибутив Java. Начиная с весны прошлого года, Либерику делает американская компания BELLSOFT, и без VPN даже нельзя открыть экран загрузки.

    Отечественный дистрибутив называется Axiom JDK. Часть разработчиков Аксиомы — это те же люди, что когда-то делали Либерику.