Skip to content

Instantly share code, notes, and snippets.

@nllsdfx
Last active March 23, 2026 08:01
Show Gist options
  • Select an option

  • Save nllsdfx/1bbad663b10201fa014155c4ae98fda3 to your computer and use it in GitHub Desktop.

Select an option

Save nllsdfx/1bbad663b10201fa014155c4ae98fda3 to your computer and use it in GitHub Desktop.
/**
* Создает новую сущность Employment на основе приглашения.
*
* @param invite приглашение, на основе которого создается занятость
* @param now текущее время
* @param versionId идентификатор версии
* @return новая сущность Employment
*/
@NonNull
public static Employment createFromInvite(@NonNull Invite invite,
@NonNull Instant now,
@NonNull Long versionId) {
Employment employment = new Employment();
employment.setEmployee(invite.getEmployee());
employment.setClientProfile(invite.getClientProfile());
employment.setExpirationDate(invite.getInviteDetail().getExpirationDate());
employment.setCreatedAt(now);
employment.setUpdatedAt(now);
EmploymentStatus status = EmploymentStatus.builder()
.createdAt(now)
.employment(employment)
.typeId(EmploymentStatusTypeName.AWAITING_APPLICATION_APPROVAL.getId())
.build();
employment.statuses.add(status);
var version = EmploymentVersion.createNewVersion(employment,
EmploymentRoleCreatorName.EMPLOYEE.getId(), now, versionId,
invite);
employment.versions.add(version);
return employment;
}
/**
* Сервис создания записей занятости (Employment) на основе инвайта.
*/
public interface EmploymentCreationService {
/**
* Создаёт запись Employment на основе переданного инвайта.
*
* @param invite инвайт, на основе которого создаётся Employment
* @return идентификатор созданной записи Employment
*/
@NonNull
Long createFromInvite(@NonNull Invite invite);
}
/**
* {@inheritDoc}
*/
@Service
@RequiredArgsConstructor
class EmploymentCreationServiceImpl implements EmploymentCreationService {
private final DateTimeService dateTimeService;
private final SequenceGenerator sequenceGenerator;
private final EmploymentApplicationService applicationService;
private final EmploymentRepository repository;
private final EmploymentExportService employmentExportService;
@Override
@Transactional
public @NonNull Long createFromInvite(@NonNull Invite invite) {
var now = dateTimeService.nowInstant();
var versionId = sequenceGenerator.getNextSequence(
SequenceGenerator.EMPLOYMENT_VERSION_SEQUENCE);
var employment = Employment.createFromInvite(invite, now, versionId);
employment = repository.save(employment);
applicationService.createApplicationFromEmployment(employment.getId(), now);
// Экспортируем сведения о трудоустройстве через outbox
employmentExportService.exportEmployment(employment, invite.getId());
return employment.getId();
}
}
@Service
@RequiredArgsConstructor
class EmploymentExportServiceImpl implements EmploymentExportService {
private final KafkaTopicsProperties kafkaTopicsProperties;
private final TemporaryTextStorageService temporaryTextStorageService;
private final OutboxService outboxService;
private final EmploymentMessageMapper employmentMessageMapper;
@Override
@Transactional
public void exportEmployment(@NonNull Employment employment, @NonNull Long inviteId) {
Short statusId = employment.getStatuses().stream()
.max(Comparator.comparing(EmploymentStatus::getCreatedAt))
.map(EmploymentStatus::getTypeId)
.orElseThrow();
EmploymentVersion activeVersion = employment.getVersions().stream()
.filter(v -> Boolean.TRUE.equals(v.getActive()))
.findFirst()
.orElseThrow();
EmploymentPosition position = activeVersion.getPositions().stream().findFirst()
.orElseThrow();
EmploymentSalary salary = activeVersion.getSalaries().stream().findFirst()
.orElseThrow();
EmploymentConditions conditions = activeVersion.getConditions().stream().findFirst()
.orElseThrow();
EmploymentMessage message = employmentMessageMapper.toMessage(
employment,
statusId,
activeVersion,
position,
salary,
conditions,
inviteId
);
TemporaryTextData textData = temporaryTextStorageService.saveData(message);
var kafkaData = new SendKafkaData(kafkaTopicsProperties.getEmployment(),
employment.getId().toString());
outboxService.createOutbox(SEND_TO_KAFKA.getId(), kafkaData, textData);
}
}
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2025-10-22T14:08:34.831722+03:00[Europe/Moscow]", comments = "Generator version: 7.13.0")
@Validated
public interface InviteApi {
/**
* GET /api/v1/invite/{invite_id} : Получение инвайта
* Получение инвайта по ID
*
* @param inviteId Внутренний идентификатор инвайта (required)
* @return Успешное получение информации об инвайте (status code 200)
* or Неверные входные данные (status code 400)
* or Неавторизованный доступ (status code 401)
* or Нет доступа (status code 403)
*/
@RequestMapping(
method = RequestMethod.GET,
value = "/api/v1/invite/{invite_id}",
produces = { "application/json" }
)
ResponseEntity<GetInviteById200Response> getInviteById(
@Min(1L) @PathVariable("invite_id") Long inviteId
);
/**
* GET /api/v1/invites : Получение списка инвайтов
* Получение списка инвайтов сотрудника
*
* @return Успешное получение информации об инвайтах (status code 200)
* or Ошибка авторизации (status code 401)
*/
@RequestMapping(
method = RequestMethod.GET,
value = "/api/v1/invites",
produces = { "application/json" }
)
ResponseEntity<GetInvites200Response> getInvites(
final Pageable pageable
);
/**
* PATCH /api/v1/invite/{invite_id} : Отправка инвайта повторно или отзыв
* Отправка инвайта повторно или отзыв
*
* @param inviteId Внутренний идентификатор инвайта (required)
* @param patchInviteStatusRequest (required)
* @return Успешное изменение статуса инвайта (status code 200)
* or Неверные входные данные (status code 400)
* or Неавторизованный доступ (status code 401)
* or Нет доступа (status code 403)
*/
@RequestMapping(
method = RequestMethod.PATCH,
value = "/api/v1/invite/{invite_id}",
produces = { "application/json" },
consumes = { "application/json" }
)
ResponseEntity<PatchInviteStatus200Response> patchInviteStatus(
@Min(1L) @PathVariable("invite_id") Long inviteId,
@Valid @RequestBody PatchInviteStatusRequest patchInviteStatusRequest
);
}
/**
* Контроллер для работы с инвайтами пользователя. Предоставляет REST API для получения информации
* об инвайтах.
*/
@RestController
@RequiredArgsConstructor
public class InviteController implements InviteApi {
private final InviteService inviteService;
private final InviteMapper inviteMapper;
private final AuthService authService;
/**
* Получить инвайт по ID.
*/
@Override
public ResponseEntity<GetInviteById200Response> getInviteById(Long inviteId) {
var userId = authService.getCurrentUserId();
return ResponseEntity.ok(
inviteMapper.mapInviteById(inviteService.findByInviteIdAndEmployeeId(inviteId, userId)));
}
/**
* Получить список всех инвайтов пользователя.
*/
@Override
public ResponseEntity<GetInvites200Response> getInvites(Pageable pageable) {
var userId = authService.getCurrentUserId();
var invites = inviteService.getAllInvites(userId, pageable);
return ResponseEntity.ok(inviteMapper.mapInvitesListInner(invites));
}
/**
* Обновить статус инвайта.
*/
@Override
public ResponseEntity<PatchInviteStatus200Response> patchInviteStatus(Long inviteId,
PatchInviteStatusRequest patchInviteByIdRequest) {
var userId = authService.getCurrentUserId();
var patchedInviteId = inviteService.patchInviteStatus(
inviteId,
userId,
inviteMapper.mapInviteStatus(patchInviteByIdRequest.getStatus()));
return ResponseEntity.ok(inviteMapper.mapInviteIdToPatchInviteStatusDto(patchedInviteId));
}
}

Руководство по проекту Название

Общие принципы

  • Все комментарии в коде должны быть на русском языке
  • Следуйте правилам checkstyle от Google Checks
  • Следуй практикам SOLID
  • Для работы с датами и временем всегда используйте DateTimeService
  • При создании нового сервиса всегда сначала создавайте интерфейс, затем реализацию
  • Если не хватает каких-то репозиториев или сервисов, создавайте их
  • Старайся переиспользовать код, если возможно
  • Пиши Javadoc для публичных классов и методов на русском языке

Архитектура проекта

Проект следует многослойной архитектуре:

  1. Controller - обрабатывает HTTP-запросы, делегирует бизнес-логику сервисам
  2. Service - содержит бизнес-логику
  3. Repository - обеспечивает доступ к данным
  4. Entity - модели данных для хранения в БД
  5. Модуль api - только контроллеры и дто, мапперы между entity и rest-dto
  6. Модуль persistence - jpa-репозитории и entity
  7. Модуль service - бизнес логика (сервисы)

Контроллеры

  • Контроллеры должны реализовывать интерфейсы, сгенерированные из OpenAPI спецификаций
  • Слой контроллеров не должен содержать бизнес-логику
  • Для преобразования между DTO и Entity используйте MapStruct маппер
  • Для контроллеров обязательно пишите Unit & Happy path тесты
  • Сверяй валидацию входящих параметров с уже готовой валидацией в интерфейсах, сгенерированных из OpenAPI спецификаций

Сервисы

  • Сервисы должны иметь интерфейс и реализацию
  • Сервисы не должны знать о типах запросов или ответов в контроллерах
  • Используйте существующие сервисы вместо прямого обращения к репозиториям, когда такие сервисы доступны (например, используйте ClientProfileService вместо ClientProfileRepository)
  • Для всей логики сервисов пишите unit-тесты
  • Используйте конструктор с @RequiredArgsConstructor для внедрения зависимостей
  • Для транзакционных методов используйте аннотацию @Transactional

Маппинг объектов

  • Используйте MapStruct для преобразования между Entity и DTO
  • Маппер должен быть интерфейсом с аннотацией @Mapper(componentModel = "spring")
  • Для сложных преобразований используйте @Mapping аннотации
  • Пиши unit-тесты для слоя мапперов

Обработка исключений

  • Для бизнес-исключений используйте BusinessException с соответствующим BusinessExceptionReason
  • При добавлении новых типов ошибок обновляйте ExceptionReasonHttpStatusMapperImpl для маппинга на HTTP-статусы
  • Используйте глобальный обработчик исключений для преобразования исключений в HTTP-ответы

Работа с датами и временем

  • Всегда используйте DateTimeService для работы с датами и временем
  • Не используйте напрямую LocalDateTime.now(), Instant.now() и т.п.
  • Для хранения времени в БД используйте LocalDateTime или OffsetDateTime
  • Для API взаимодействий используйте Instant

Сборка

  • Проект мультимодульный, поэтому для сборки проекта и запусков тестов нужно собирать полностью root-модуль, включая все дочерние модули (название модуля)

Тестирование

Unit-тесты

  • Пишите unit-тесты для всей бизнес-логики в сервисах, для мапперов, утилит, и контроллеров (с happy path)
  • Используйте JUnit 5 и Mockito
  • Следуйте паттерну Arrange-Act-Assert
  • Тестируйте как позитивные, так и негативные сценарии
  • Используйте понятные имена тестов, описывающие сценарий и ожидаемый результат

Именование

  • Имена классов, методов и переменных должны быть на английском языке
  • Комментарии должны быть на русском языке
  • Интеграционные тесты должны иметь суффикс IT
  • Unit-тесты должны иметь суффикс Test
  • Реализации интерфейсов должны иметь суффикс Impl
  • Реализации интерфейсов должны быть package private

Документация

  • Все публичные методы должны иметь JavaDoc комментарии на русском языке
  • Документируйте параметры, возвращаемые значения и исключения
  • Для реализаций интерфейсов используйте {@inheritDoc}
  • API должно быть описано в OpenAPI спецификациях в формате YAML

Процесс разработки

  1. Создайте интерфейс сервиса с Javadoc
  2. Реализуйте интерфейс
  3. Напишите unit-тесты для сервиса
  4. Создайте или обновите контроллер
  5. Создайте или обновите маппер
  6. Напишите или обновите тесты
  7. Проверьте соответствие правилам checkstyle
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment