22 KiB
📝 Задача: Система управления событиями
Условие:
Вы разрабатываете систему для управления событиями (конференции, вебинары, встречи). Нужно реализовать сервис регистрации участников с учетом следующих требований:
-
Событие имеет:
- ID, название, описание
- Дату начала и окончания
- Максимальное количество участников
- Статус (DRAFT, PUBLISHED, CANCELLED)
-
Участник имеет:
- ID, имя, email
- Дату регистрации
-
Бизнес-правила:
- Регистрация возможна только на опубликованные события
- Нельзя зарегистрироваться на событие, которое уже началось
- Нельзя превысить лимит участников
- Один участник может зарегистрироваться на событие только один раз
- При отмене события нужно уведомить всех участников
Техническое задание:
Реализуйте следующие компоненты:
-
Entity классы (Event, Participant, Registration)
-
Repository интерфейсы
-
Service класс EventRegistrationService с методами:
registerParticipant(Long eventId, Long participantId)cancelRegistration(Long eventId, Long participantId)cancelEvent(Long eventId)getEventParticipants(Long eventId)
-
REST Controller с endpoint'ами для регистрации
-
Обработка исключений для всех бизнес-правил
Дополнительные требования:
- Использовать Spring Boot, JPA/Hibernate
- Добавить валидацию данных
- Написать unit-тесты для сервиса
- Подумать о транзакциях и concurrency
Время на выполнение: 25 минут
🧮 Алгоритмическая задача для Middle разработчика
📝 Задача: Планировщик встреч
Условие:
У вас есть список встреч, каждая встреча представлена как массив [start, end], где start и end - время начала и окончания в минутах от начала дня.
Нужно найти максимальное количество непересекающихся встреч, которые можно провести в одной переговорной комнате.
Примеры:
Пример 1:
Ввод: meetings = [[0,30],[5,10],[15,20]]
Вывод: 2
Объяснение: Можно выбрать встречи [0,30] и [15,20] ИЛИ [5,10] и [15,20]
Пример 2:
Ввод: meetings = [[7,10],[2,4]]
Вывод: 2
Объяснение: Обе встречи не пересекаются
Пример 3:
Ввод: meetings = [[1,5],[8,9],[8,9],[5,9],[9,15]]
Вывод: 3
Объяснение: Можно выбрать [1,5], [8,9], [9,15]
Ограничения:
1 <= meetings.length <= 10^4meetings[i].length == 20 <= starti < endi <= 10^6
Задание:
- Реализуйте метод
public int maxMeetings(int[][] meetings) - Объясните алгоритм и его сложность
- Напишите unit-тесты
Время на выполнение: 15 минут
✅ Решения задач
💼 Решение прикладной задачи
Показать решение
1. Entity классы:
@Entity
@Table(name = "events")
public class Event {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private String description;
@Column(name = "start_date", nullable = false)
private LocalDateTime startDate;
@Column(name = "end_date", nullable = false)
private LocalDateTime endDate;
@Column(name = "max_participants", nullable = false)
private Integer maxParticipants;
@Enumerated(EnumType.STRING)
private EventStatus status;
@OneToMany(mappedBy = "event", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Registration> registrations = new ArrayList<>();
// конструкторы, геттеры, сеттеры
}
@Entity
@Table(name = "participants")
public class Participant {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
// конструкторы, геттеры, сеттеры
}
@Entity
@Table(name = "registrations")
public class Registration {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "event_id", nullable = false)
private Event event;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "participant_id", nullable = false)
private Participant participant;
@Column(name = "registration_date", nullable = false)
private LocalDateTime registrationDate;
// конструкторы, геттеры, сеттеры
}
public enum EventStatus {
DRAFT, PUBLISHED, CANCELLED
}
2. Repository интерфейсы:
@Repository
public interface EventRepository extends JpaRepository<Event, Long> {
List<Event> findByStatus(EventStatus status);
}
@Repository
public interface ParticipantRepository extends JpaRepository<Participant, Long> {
Optional<Participant> findByEmail(String email);
}
@Repository
public interface RegistrationRepository extends JpaRepository<Registration, Long> {
boolean existsByEventIdAndParticipantId(Long eventId, Long participantId);
Optional<Registration> findByEventIdAndParticipantId(Long eventId, Long participantId);
@Query("SELECT r.participant FROM Registration r WHERE r.event.id = :eventId")
List<Participant> findParticipantsByEventId(@Param("eventId") Long eventId);
long countByEventId(Long eventId);
void deleteByEventId(Long eventId);
}
3. Service класс:
@Service
@Transactional
public class EventRegistrationService {
private final EventRepository eventRepository;
private final ParticipantRepository participantRepository;
private final RegistrationRepository registrationRepository;
private final NotificationService notificationService;
public EventRegistrationService(EventRepository eventRepository,
ParticipantRepository participantRepository,
RegistrationRepository registrationRepository,
NotificationService notificationService) {
this.eventRepository = eventRepository;
this.participantRepository = participantRepository;
this.registrationRepository = registrationRepository;
this.notificationService = notificationService;
}
public void registerParticipant(Long eventId, Long participantId) {
Event event = eventRepository.findById(eventId)
.orElseThrow(() -> new EventNotFoundException("Event not found: " + eventId));
Participant participant = participantRepository.findById(participantId)
.orElseThrow(() -> new ParticipantNotFoundException("Participant not found: " + participantId));
validateRegistration(event, participant);
Registration registration = new Registration();
registration.setEvent(event);
registration.setParticipant(participant);
registration.setRegistrationDate(LocalDateTime.now());
registrationRepository.save(registration);
}
private void validateRegistration(Event event, Participant participant) {
// Проверка статуса события
if (event.getStatus() != EventStatus.PUBLISHED) {
throw new RegistrationException("Event is not published");
}
// Проверка даты начала
if (event.getStartDate().isBefore(LocalDateTime.now())) {
throw new RegistrationException("Event has already started");
}
// Проверка лимита участников
long currentParticipants = registrationRepository.countByEventId(event.getId());
if (currentParticipants >= event.getMaxParticipants()) {
throw new RegistrationException("Event is full");
}
// Проверка дублирования регистрации
if (registrationRepository.existsByEventIdAndParticipantId(event.getId(), participant.getId())) {
throw new RegistrationException("Participant already registered for this event");
}
}
public void cancelRegistration(Long eventId, Long participantId) {
Registration registration = registrationRepository.findByEventIdAndParticipantId(eventId, participantId)
.orElseThrow(() -> new RegistrationNotFoundException("Registration not found"));
registrationRepository.delete(registration);
}
public void cancelEvent(Long eventId) {
Event event = eventRepository.findById(eventId)
.orElseThrow(() -> new EventNotFoundException("Event not found: " + eventId));
List<Participant> participants = registrationRepository.findParticipantsByEventId(eventId);
// Отмена события
event.setStatus(EventStatus.CANCELLED);
eventRepository.save(event);
// Уведомление участников
participants.forEach(participant ->
notificationService.notifyEventCancellation(participant, event));
}
@Transactional(readOnly = true)
public List<Participant> getEventParticipants(Long eventId) {
if (!eventRepository.existsById(eventId)) {
throw new EventNotFoundException("Event not found: " + eventId);
}
return registrationRepository.findParticipantsByEventId(eventId);
}
}
4. REST Controller:
@RestController
@RequestMapping("/api/events")
@Validated
public class EventRegistrationController {
private final EventRegistrationService registrationService;
public EventRegistrationController(EventRegistrationService registrationService) {
this.registrationService = registrationService;
}
@PostMapping("/{eventId}/participants/{participantId}")
public ResponseEntity<Void> registerParticipant(
@PathVariable @Positive Long eventId,
@PathVariable @Positive Long participantId) {
registrationService.registerParticipant(eventId, participantId);
return ResponseEntity.ok().build();
}
@DeleteMapping("/{eventId}/participants/{participantId}")
public ResponseEntity<Void> cancelRegistration(
@PathVariable @Positive Long eventId,
@PathVariable @Positive Long participantId) {
registrationService.cancelRegistration(eventId, participantId);
return ResponseEntity.ok().build();
}
@DeleteMapping("/{eventId}")
public ResponseEntity<Void> cancelEvent(@PathVariable @Positive Long eventId) {
registrationService.cancelEvent(eventId);
return ResponseEntity.ok().build();
}
@GetMapping("/{eventId}/participants")
public ResponseEntity<List<ParticipantDto>> getEventParticipants(
@PathVariable @Positive Long eventId) {
List<Participant> participants = registrationService.getEventParticipants(eventId);
List<ParticipantDto> dtos = participants.stream()
.map(this::toDto)
.collect(Collectors.toList());
return ResponseEntity.ok(dtos);
}
private ParticipantDto toDto(Participant participant) {
return new ParticipantDto(participant.getId(), participant.getName(), participant.getEmail());
}
}
5. Exception Handling:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EventNotFoundException.class)
public ResponseEntity<ErrorResponse> handleEventNotFound(EventNotFoundException ex) {
ErrorResponse error = new ErrorResponse("EVENT_NOT_FOUND", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(RegistrationException.class)
public ResponseEntity<ErrorResponse> handleRegistrationError(RegistrationException ex) {
ErrorResponse error = new ErrorResponse("REGISTRATION_ERROR", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handleValidation(ConstraintViolationException ex) {
ErrorResponse error = new ErrorResponse("VALIDATION_ERROR", "Invalid input parameters");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
}
6. Unit тесты:
@ExtendWith(MockitoExtension.class)
class EventRegistrationServiceTest {
@Mock
private EventRepository eventRepository;
@Mock
private ParticipantRepository participantRepository;
@Mock
private RegistrationRepository registrationRepository;
@Mock
private NotificationService notificationService;
@InjectMocks
private EventRegistrationService service;
@Test
void registerParticipant_Success() {
// Given
Long eventId = 1L;
Long participantId = 1L;
Event event = createPublishedEvent();
Participant participant = createParticipant();
when(eventRepository.findById(eventId)).thenReturn(Optional.of(event));
when(participantRepository.findById(participantId)).thenReturn(Optional.of(participant));
when(registrationRepository.countByEventId(eventId)).thenReturn(5L);
when(registrationRepository.existsByEventIdAndParticipantId(eventId, participantId)).thenReturn(false);
// When
service.registerParticipant(eventId, participantId);
// Then
verify(registrationRepository).save(any(Registration.class));
}
@Test
void registerParticipant_EventNotPublished_ThrowsException() {
// Given
Event event = createDraftEvent();
Participant participant = createParticipant();
when(eventRepository.findById(1L)).thenReturn(Optional.of(event));
when(participantRepository.findById(1L)).thenReturn(Optional.of(participant));
// When & Then
assertThrows(RegistrationException.class,
() -> service.registerParticipant(1L, 1L));
}
@Test
void registerParticipant_EventFull_ThrowsException() {
// Given
Event event = createPublishedEvent();
event.setMaxParticipants(10);
Participant participant = createParticipant();
when(eventRepository.findById(1L)).thenReturn(Optional.of(event));
when(participantRepository.findById(1L)).thenReturn(Optional.of(participant));
when(registrationRepository.countByEventId(1L)).thenReturn(10L);
// When & Then
assertThrows(RegistrationException.class,
() -> service.registerParticipant(1L, 1L));
}
private Event createPublishedEvent() {
Event event = new Event();
event.setId(1L);
event.setStatus(EventStatus.PUBLISHED);
event.setStartDate(LocalDateTime.now().plusDays(1));
event.setMaxParticipants(10);
return event;
}
private Event createDraftEvent() {
Event event = new Event();
event.setId(1L);
event.setStatus(EventStatus.DRAFT);
event.setStartDate(LocalDateTime.now().plusDays(1));
event.setMaxParticipants(10);
return event;
}
private Participant createParticipant() {
Participant participant = new Participant();
participant.setId(1L);
participant.setName("John Doe");
participant.setEmail("john@example.com");
return participant;
}
}
🧮 Решение алгоритмической задачи
Показать решение
Основная идея:
Это классическая задача о максимальном количестве непересекающихся интервалов. Используем жадный алгоритм:
- Сортируем встречи по времени окончания
- Выбираем встречу с самым ранним окончанием
- Исключаем все пересекающиеся с ней встречи
- Повторяем для оставшихся встреч
Решение:
import java.util.*;
public class MeetingScheduler {
public int maxMeetings(int[][] meetings) {
if (meetings == null || meetings.length == 0) {
return 0;
}
// Сортируем по времени окончания
Arrays.sort(meetings, (a, b) -> Integer.compare(a[1], b[1]));
int count = 0;
int lastEndTime = -1;
for (int[] meeting : meetings) {
int startTime = meeting[0];
int endTime = meeting[1];
// Если текущая встреча не пересекается с предыдущей выбранной
if (startTime >= lastEndTime) {
count++;
lastEndTime = endTime;
}
}
return count;
}
}
Объяснение алгоритма:
-
Сортировка: Сортируем все встречи по времени окончания. Это ключевая идея - выбирая встречи с самым ранним окончанием, мы оставляем максимум времени для последующих встреч.
-
Жадный выбор: Проходим по отсортированным встречам и выбираем те, которые не пересекаются с уже выбранными.
-
Условие непересечения: Встреча не пересекается с предыдущей, если её время начала >= времени окончания предыдущей.
Временная сложность: O(n log n) - из-за сортировки
Пространственная сложность: O(1) - если не считать пространство для сортировки
Пошаговый пример:
Для meetings = [[1,5],[8,9],[8,9],[5,9],[9,15]]:
- После сортировки по времени окончания:
[[1,5], [8,9], [8,9], [5,9], [9,15]] - Выбираем
[1,5],lastEndTime = 5 - Проверяем
[8,9]:8 >= 5✓, выбираем,lastEndTime = 9 - Проверяем
[8,9]:8 < 9✗, пропускаем - Проверяем
[5,9]:5 < 9✗, пропускаем - Проверяем
[9,15]:9 >= 9✓, выбираем - Результат: 3 встречи
Unit тесты:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MeetingSchedulerTest {
private final MeetingScheduler scheduler = new MeetingScheduler();
@Test
void testExample1() {
int[][] meetings = {{0,30},{5,10},{15,20}};
assertEquals(2, scheduler.maxMeetings(meetings));
}
@Test
void testExample2() {
int[][] meetings = {{7,10},{2,4}};
assertEquals(2, scheduler.maxMeetings(meetings));
}
@Test
void testExample3() {
int[][] meetings = {{1,5},{8,9},{8,9},{5,9},{9,15}};
assertEquals(3, scheduler.maxMeetings(meetings));
}
@Test
void testEmptyInput() {
int[][] meetings = {};
assertEquals(0, scheduler.maxMeetings(meetings));
}
@Test
void testNullInput() {
assertEquals(0, scheduler.maxMeetings(null));
}
@Test
void testSingleMeeting() {
int[][] meetings = {{1,3}};
assertEquals(1, scheduler.maxMeetings(meetings));
}
@Test
void testAllOverlapping() {
int[][] meetings = {{1,4},{2,5},{3,6}};
assertEquals(1, scheduler.maxMeetings(meetings));
}
@Test
void testNoOverlapping() {
int[][] meetings = {{1,2},{3,4},{5,6}};
assertEquals(3, scheduler.maxMeetings(meetings));
}
@Test
void testTouchingMeetings() {
int[][] meetings = {{1,3},{3,5},{5,7}};
assertEquals(3, scheduler.maxMeetings(meetings));
}
}
Альтернативные подходы:
- Dynamic Programming: Сложность O(n²), не оптимально для этой задачи
- Recursive with memoization: Также O(n²), избыточно
- Priority Queue: Можно использовать, но жадный алгоритм проще и эффективнее
Почему жадный алгоритм работает:
Выбирая встречу с самым ранним окончанием, мы всегда оставляем максимально возможное время для размещения остальных встреч. Это оптимальная стратегия для данной задачи.