644 lines
22 KiB
Markdown
644 lines
22 KiB
Markdown
## 📝 Задача: Система управления событиями
|
||
|
||
### **Условие:**
|
||
|
||
Вы разрабатываете систему для управления событиями (конференции, вебинары, встречи). Нужно реализовать сервис регистрации участников с учетом следующих требований:
|
||
|
||
1. **Событие** имеет:
|
||
|
||
- ID, название, описание
|
||
- Дату начала и окончания
|
||
- Максимальное количество участников
|
||
- Статус (DRAFT, PUBLISHED, CANCELLED)
|
||
2. **Участник** имеет:
|
||
|
||
- ID, имя, email
|
||
- Дату регистрации
|
||
3. **Бизнес-правила:**
|
||
|
||
- Регистрация возможна только на опубликованные события
|
||
- Нельзя зарегистрироваться на событие, которое уже началось
|
||
- Нельзя превысить лимит участников
|
||
- Один участник может зарегистрироваться на событие только один раз
|
||
- При отмене события нужно уведомить всех участников
|
||
|
||
### **Техническое задание:**
|
||
|
||
Реализуйте следующие компоненты:
|
||
|
||
1. **Entity классы** (Event, Participant, Registration)
|
||
|
||
2. **Repository интерфейсы**
|
||
|
||
3. **Service класс EventRegistrationService** с методами:
|
||
|
||
- `registerParticipant(Long eventId, Long participantId)`
|
||
- `cancelRegistration(Long eventId, Long participantId)`
|
||
- `cancelEvent(Long eventId)`
|
||
- `getEventParticipants(Long eventId)`
|
||
4. **REST Controller** с endpoint'ами для регистрации
|
||
|
||
5. **Обработка исключений** для всех бизнес-правил
|
||
|
||
|
||
### **Дополнительные требования:**
|
||
|
||
- Использовать 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^4`
|
||
- `meetings[i].length == 2`
|
||
- `0 <= starti < endi <= 10^6`
|
||
|
||
### **Задание:**
|
||
|
||
1. Реализуйте метод `public int maxMeetings(int[][] meetings)`
|
||
2. Объясните алгоритм и его сложность
|
||
3. Напишите unit-тесты
|
||
|
||
**Время на выполнение:** 15 минут
|
||
|
||
---
|
||
|
||
# ✅ Решения задач
|
||
|
||
## 💼 Решение прикладной задачи
|
||
|
||
<details> <summary>Показать решение</summary>
|
||
|
||
### **1. Entity классы:**
|
||
|
||
```java
|
||
@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 интерфейсы:**
|
||
|
||
```java
|
||
@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 класс:**
|
||
|
||
```java
|
||
@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:**
|
||
|
||
```java
|
||
@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:**
|
||
|
||
```java
|
||
@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 тесты:**
|
||
|
||
```java
|
||
@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;
|
||
}
|
||
}
|
||
```
|
||
|
||
</details>
|
||
|
||
---
|
||
|
||
## 🧮 Решение алгоритмической задачи
|
||
|
||
<details> <summary>Показать решение</summary>
|
||
|
||
### **Основная идея:**
|
||
|
||
Это классическая задача о максимальном количестве непересекающихся интервалов. Используем жадный алгоритм:
|
||
|
||
1. Сортируем встречи по времени окончания
|
||
2. Выбираем встречу с самым ранним окончанием
|
||
3. Исключаем все пересекающиеся с ней встречи
|
||
4. Повторяем для оставшихся встреч
|
||
|
||
### **Решение:**
|
||
|
||
```java
|
||
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;
|
||
}
|
||
}
|
||
```
|
||
|
||
### **Объяснение алгоритма:**
|
||
|
||
1. **Сортировка:** Сортируем все встречи по времени окончания. Это ключевая идея - выбирая встречи с самым ранним окончанием, мы оставляем максимум времени для последующих встреч.
|
||
|
||
2. **Жадный выбор:** Проходим по отсортированным встречам и выбираем те, которые не пересекаются с уже выбранными.
|
||
|
||
3. **Условие непересечения:** Встреча не пересекается с предыдущей, если её время начала >= времени окончания предыдущей.
|
||
|
||
|
||
### **Временная сложность:** O(n log n) - из-за сортировки
|
||
|
||
### **Пространственная сложность:** O(1) - если не считать пространство для сортировки
|
||
|
||
### **Пошаговый пример:**
|
||
|
||
Для `meetings = [[1,5],[8,9],[8,9],[5,9],[9,15]]`:
|
||
|
||
1. После сортировки по времени окончания: `[[1,5], [8,9], [8,9], [5,9], [9,15]]`
|
||
2. Выбираем `[1,5]`, `lastEndTime = 5`
|
||
3. Проверяем `[8,9]`: `8 >= 5` ✓, выбираем, `lastEndTime = 9`
|
||
4. Проверяем `[8,9]`: `8 < 9` ✗, пропускаем
|
||
5. Проверяем `[5,9]`: `5 < 9` ✗, пропускаем
|
||
6. Проверяем `[9,15]`: `9 >= 9` ✓, выбираем
|
||
7. Результат: 3 встречи
|
||
|
||
### **Unit тесты:**
|
||
|
||
```java
|
||
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));
|
||
}
|
||
}
|
||
```
|
||
|
||
### **Альтернативные подходы:**
|
||
|
||
1. **Dynamic Programming:** Сложность O(n²), не оптимально для этой задачи
|
||
2. **Recursive with memoization:** Также O(n²), избыточно
|
||
3. **Priority Queue:** Можно использовать, но жадный алгоритм проще и эффективнее
|
||
|
||
### **Почему жадный алгоритм работает:**
|
||
|
||
Выбирая встречу с самым ранним окончанием, мы всегда оставляем максимально возможное время для размещения остальных встреч. Это оптимальная стратегия для данной задачи.
|
||
|
||
</details> |