feat: Initial commit

This commit is contained in:
Anastasia Tarazevich 2026-02-23 17:50:23 +03:00
commit 05522dd0c3
33 changed files with 829 additions and 0 deletions

96
README.md Normal file
View File

@ -0,0 +1,96 @@
# НТО 2025. II отборочный этап. Командные задания — Backend Решение
## 📖 Предыстория
В компании S есть возможность бронирования мест в пространствах, предназначенных под общее использование (open-space). На данный момент для бронирования места используются различные способы бронирования, разработанные в каждом офисе индивидуально.
Администрации компании S требуется мобильное приложение, как для рядовых сотрудников, так и для администрации с возможностью просмотра забронированных мест.
## 📑 Технологический стек
- Java 17
- Spring Boot
- H2
- Liquibase
## 🛠️ Техническое задание
Требуется разработать серверное приложение на Java с использованием Spring Boot, которое работает на основе протокола HTTP и взаимодействует с клиентами благодаря RESTful API.
Для хранения данных о сотрудниках и их посещениях должна использоваться реляционная база данных (H2). Схема БД должна создаваться liquibase-скриптами. ID-поля всех сущностей, сохраняемых в базе, должны выдаваться на уровне БД. Стратегия генерации ID - автоинкремент (1, 2, 3, 4…)
Сотрудникам не нужно регистрироваться, все данные в базе должны быть предзаполнены liquibase-скриптами при запуске серверного приложения. Данные для предзаполнения таблиц представлены ниже.
Картинки для аватаров пользователей не должны храниться в БД. Должны быть сохранены лишь URL-адреса на ресурсы, откуда в последующем мобильное приложение загрузит соответствующее изображение.
Сервер разрабатывается на основе предоставляемой заготовки проекта. Версии зависимостей и сами зависимости изменяться не должны.
## 📂 Правила работы с проектом-заготовкой
В предоставленном проекте необходимо изучить, но никак не модифицировать, не перемещать и не удалять следующие файлы:
- `pom.xml`
- `application.yml`
- все файлы из `db.changelog`
Кроме описанных выше файлов, в проекте уже созданы основные классы-сущности и добавлены пустые классы всех слоев. В этих классах необходимо будет написать программный код и добавить аннотации для реализации описанного задания. Наименования классов и прочий код уже написанный в предоставляемом проекте **изменять/удалять не нужно, необходимо их доработать**. Добавлять свои дополнительные классы в проект можно.
Создание таблиц в БД и предзаполнение их требуемыми данными уже реализовано в заготовке при помощи liquibase. В одной из сущностей добавлены аннотации для реализации связи “один ко многим”, обратите внимание, что в проекте потребуется еще связь данного типа.
## 🌐 Где необходимо разместить сервер
Серверное приложение должно быть разработано и протестировано локально (**не требуется** размещать сервер удаленно и осуществлять его функционирование 24/7).
## 📋 Технические требования к серверу и его ответам клиенту
Для конфигурирования Вашего сервера (его хоста/IP адреса) используйте константы из файла `Constants.kt`.
| **Тип запроса** | **Путь** | **Описание** | **Параметры/Тело** | **Ответы** |
|-----------------|-----------------------|---------------------------|--------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **GET** | `api/<CODE>/auth` | Проверка авторизации | `<CODE>` - код для входа | `400` - что-то пошло не так<br>`401` - кода не существует<br>`200` - данный код существует - можно пользоваться приложением |
| **GET** | `api/<CODE>/info` | Получение информации о пользователе | `<CODE>` - код для входа | `400` - что-то пошло не так<br>`401` - кода не существует<br>`200` - ОК<br><pre>{<br> "name":"Иванов Петр Федорович",<br> "photoUrl":"<https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg>",<br> "booking":{<br> "2025-01-05": {"id":1,"place":"102"},<br> "2025-01-06": {"id":2,"place":"209.13"},<br> "2025-01-09": {"id":3,"place":"Зона 51. 50"}<br> }<br>}</pre> |
| **GET** | `api/<CODE>/booking` | Получение доступных для бронирования мест | `<CODE>` - код для входа | `400` - что-то пошло не так<br>`401` - кода не существует<br>`200` - ОК<br><pre>{<br> "2025-01-05": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}],<br> "2025-01-06": [{"id": 3, "place": "Зона 51. 50"}],<br> "2025-01-07": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}],<br> "2025-01-08": [{"id": 2, "place": "209.13"}]<br>}</pre> **Список дат ограничен текущим + 3 днями** (ответ от сервера содержит 4 дня со свободными местами для каждого)
| **POST** | `api/<CODE>/book` | Создание нового бронирования | `<CODE>` - код для входа<br>Тело: <br> <pre>{<br> “date”: “2025-01-05”,<br> “placeId”: 1 <br>}</pre> |`400` - что-то пошло не так<br>`401` - кода не существует<br>`409` - уже забронировано<br>`201` - бронирование успешно создано
## 📊 Пример данных
Таблица сотрудников:
| **id** | **name** | **code** | **photo_url** |
|--------|-----------|---------------------------------|---------------------------------------------------------------------------------------------|
| 1 | Ivanov Ivan | 1111 | https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg |
| 2 | Petrov Petr | 2222 | https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg |
| 3 | Kozlov Oleg | 3333 | https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg |
| 4 | Smirnova Anna | 4444 | https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg |
Таблица мест для бронирования:
| **id** | **place_name** |
|--------|-----------------|
| 1 | K-19 |
| 2 | M-16 |
| 3 | T-1 |
Таблица бронирований:
| **id** | **date** | **place_id** | **employee_id** |
|--------|-----------|--------------|------------------|
| 1 | 2025-11-08 | 1 | 1 |
| 2 | 2025-11-10 | 2 | 2 |
# 📝 Решение
Работу необходимо осуществлять в предоставленном проекте-заготовке (шаблоне).
Когда завершите разработку, создайте пулреквест и запустите workflow в учебной системе.
## ✅ Особенности оценивания
При тестировании сервера на него поочередно отправляются команды, описанные в API и ожидаются определенные корректные ответы.
Сервер и приложение тестируются независимо.

61
pom.xml Normal file
View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>NTO-2025-Backend-Team-Task</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.5</version>
</parent>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.8</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,11 @@
package com.example.nto;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}

View File

@ -0,0 +1,35 @@
package com.example.nto.controller;
import com.example.nto.controller.dto.BookingCreateDto;
import com.example.nto.controller.dto.PlaceDto;
import com.example.nto.service.BookingService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@Validated
@RestController
@RequestMapping("api")
@RequiredArgsConstructor
public class BookingController {
private final BookingService bookingService;
@GetMapping("/{code}/booking")
@ResponseStatus(code = HttpStatus.OK)
public Map<LocalDate, List<PlaceDto>> getByDate(@PathVariable String code) {
return bookingService.getFreePlace(code);
}
@PostMapping("/{code}/book")
@ResponseStatus(code = HttpStatus.CREATED)
public void create(@PathVariable String code, @RequestBody BookingCreateDto bookingCreateDto) {
bookingService.create(code, bookingCreateDto);
}
}

View File

@ -0,0 +1,29 @@
package com.example.nto.controller;
import com.example.nto.controller.dto.EmployeeDto;
import com.example.nto.service.EmployeeService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("api")
@RequiredArgsConstructor
public class EmployeeController {
private final EmployeeService employeeService;
@GetMapping("/{code}/auth")
@ResponseStatus(code = HttpStatus.OK)
public void login(@PathVariable String code) {
employeeService.auth(code);
}
@GetMapping("/{code}/info")
@ResponseStatus(code = HttpStatus.OK)
public EmployeeDto getByCode(@PathVariable String code) {
return employeeService.getByCode(code);
}
}

View File

@ -0,0 +1,21 @@
package com.example.nto.controller.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BookingCreateDto {
@NotNull
private LocalDate date;
@Positive
private long placeId;
}

View File

@ -0,0 +1,31 @@
package com.example.nto.controller.dto;
import com.example.nto.entity.Booking;
import com.example.nto.entity.Employee;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.Map;
import java.util.TreeMap;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeDto {
private String name;
private String photoUrl;
private Map<LocalDate, PlaceDto> booking;
public static EmployeeDto toDto(Employee employee) {
Map<LocalDate, PlaceDto> dtoTreeMap = new TreeMap<>();
for (Booking booking : employee.getBookingList()) {
dtoTreeMap.put(booking.getDate(), PlaceDto.toDto(booking.getPlace()));
}
return new EmployeeDto(employee.getName(), employee.getPhotoUrl(), dtoTreeMap);
}
}

View File

@ -0,0 +1,20 @@
package com.example.nto.controller.dto;
import com.example.nto.entity.Place;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PlaceDto {
private long id;
private String place;
public static PlaceDto toDto(Place place) {
return new PlaceDto(place.getId(), place.getPlace());
}
}

View File

@ -0,0 +1,33 @@
package com.example.nto.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "booking")
public class Booking {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "date")
private LocalDate date;
@ManyToOne(targetEntity = Place.class, fetch = FetchType.LAZY)
@JoinColumn(name = "place_id")
private Place place;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "employee_id")
private Employee employee;
}

View File

@ -0,0 +1,34 @@
package com.example.nto.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name")
private String name;
@Column(name = "code")
private String code;
@Column(name = "photo_url")
private String photoUrl;
@OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Booking> bookingList;
}

View File

@ -0,0 +1,23 @@
package com.example.nto.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "place")
public class Place {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "place_name")
private String place;
}

View File

@ -0,0 +1,7 @@
package com.example.nto.exception;
public class BookingAlreadyExistsException extends RuntimeException {
public BookingAlreadyExistsException(String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package com.example.nto.exception;
public class EmployeeNotFoundException extends RuntimeException {
public EmployeeNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package com.example.nto.exception;
public class PlaceNotFoundException extends RuntimeException {
public PlaceNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,32 @@
package com.example.nto.exception.handler;
import com.example.nto.exception.BookingAlreadyExistsException;
import com.example.nto.exception.EmployeeNotFoundException;
import com.example.nto.exception.PlaceNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EmployeeNotFoundException.class)
public ResponseEntity<String> handleEmployeeNotFoundException(EmployeeNotFoundException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED);
}
@ExceptionHandler(BookingAlreadyExistsException.class)
public ResponseEntity<String> handleBookingAlreadyExistsException(BookingAlreadyExistsException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT);
}
@ExceptionHandler(PlaceNotFoundException.class)
public ResponseEntity<String> handlePlaceNotFoundException(PlaceNotFoundException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGenericException(Exception e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
}

View File

@ -0,0 +1,18 @@
package com.example.nto.repository;
import com.example.nto.entity.Booking;
import com.example.nto.entity.Employee;
import com.example.nto.entity.Place;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface BookingRepository extends JpaRepository<Booking, Long> {
List<Booking> findByDateBetween(LocalDate start, LocalDate end);
Optional<Booking> findByDateAndPlace(LocalDate date, Place place);
Optional<Booking> findByDateAndEmployee(LocalDate date, Employee employee);
}

View File

@ -0,0 +1,12 @@
package com.example.nto.repository;
import com.example.nto.entity.Employee;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
@EntityGraph(attributePaths = {"bookingList", "bookingList.place"})
Optional<Employee> findByCode(String code);
}

View File

@ -0,0 +1,7 @@
package com.example.nto.repository;
import com.example.nto.entity.Place;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PlaceRepository extends JpaRepository<Place, Long> {
}

View File

@ -0,0 +1,15 @@
package com.example.nto.service;
import com.example.nto.controller.dto.BookingCreateDto;
import com.example.nto.controller.dto.PlaceDto;
import com.example.nto.entity.Booking;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
public interface BookingService {
Map<LocalDate, List<PlaceDto>> getFreePlace(String code);
Booking create(String code, BookingCreateDto bookingCreateDto);
}

View File

@ -0,0 +1,9 @@
package com.example.nto.service;
import com.example.nto.controller.dto.EmployeeDto;
public interface EmployeeService {
EmployeeDto getByCode(String code);
void auth(String code);
}

View File

@ -0,0 +1,105 @@
package com.example.nto.service.impl;
import com.example.nto.controller.dto.BookingCreateDto;
import com.example.nto.controller.dto.PlaceDto;
import com.example.nto.entity.Booking;
import com.example.nto.entity.Employee;
import com.example.nto.entity.Place;
import com.example.nto.exception.BookingAlreadyExistsException;
import com.example.nto.exception.EmployeeNotFoundException;
import com.example.nto.exception.PlaceNotFoundException;
import com.example.nto.repository.BookingRepository;
import com.example.nto.repository.EmployeeRepository;
import com.example.nto.repository.PlaceRepository;
import com.example.nto.service.BookingService;
import com.example.nto.service.EmployeeService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class BookingServiceImpl implements BookingService {
private final BookingRepository bookingRepository;
private final EmployeeRepository employeeRepository;
private final PlaceRepository placeRepository;
private final EmployeeService employeeService;
@Value("${booking.days-ahead}")
private int daysAhead;
@Override
@Transactional(readOnly = true)
public Map<LocalDate, List<PlaceDto>> getFreePlace(String code) {
employeeService.auth(code);
List<Place> allPlaces = placeRepository.findAll();
LocalDate today = LocalDate.now(ZoneId.systemDefault());
LocalDate end = today.plusDays(daysAhead);
List<Booking> bookings = bookingRepository.findByDateBetween(today, end);
Map<LocalDate, Set<Long>> busyByDate = bookings.stream()
.collect(Collectors.groupingBy(
Booking::getDate,
Collectors.mapping(b -> b.getPlace().getId(), Collectors.toSet())
));
Map<LocalDate, List<PlaceDto>> result = new LinkedHashMap<>();
for (int i = 0; i <= daysAhead; i++) {
LocalDate currentDate = today.plusDays(i);
Set<Long> busyPlaces = busyByDate.getOrDefault(currentDate, Collections.emptySet());
List<PlaceDto> freePlaces = allPlaces.stream()
.filter(place -> !busyPlaces.contains(place.getId()))
.map(place -> new PlaceDto(place.getId(), place.getPlace()))
.toList();
result.put(currentDate, freePlaces);
}
return result;
}
@Override
@Transactional
public Booking create(String code, BookingCreateDto bookingCreateDto) {
LocalDate date = bookingCreateDto.getDate();
LocalDate today = LocalDate.now(ZoneId.systemDefault());
if (date.isBefore(today) || date.isAfter(today.plusDays(daysAhead))) {
throw new IllegalArgumentException("Date is out of booking window");
}
Employee employee = employeeRepository.findByCode(code)
.orElseThrow(() -> new EmployeeNotFoundException("Employee with " + code + " code not found!"));
long placeId = bookingCreateDto.getPlaceId();
Place place = placeRepository.findById(placeId)
.orElseThrow(() -> new PlaceNotFoundException("Place with " + placeId + " id not found!"));
if (bookingRepository.findByDateAndPlace(date, place).isPresent()) {
throw new BookingAlreadyExistsException("Booking already exists");
}
if (bookingRepository.findByDateAndEmployee(date, employee).isPresent()) {
throw new BookingAlreadyExistsException("This employee already has another booking on " + date);
}
Booking booking = Booking.builder()
.date(date)
.employee(employee)
.place(place)
.build();
return bookingRepository.save(booking);
}
}

View File

@ -0,0 +1,31 @@
package com.example.nto.service.impl;
import com.example.nto.controller.dto.EmployeeDto;
import com.example.nto.exception.EmployeeNotFoundException;
import com.example.nto.repository.EmployeeRepository;
import com.example.nto.service.EmployeeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class EmployeeServiceImpl implements EmployeeService {
private final EmployeeRepository employeeRepository;
@Override
@Transactional(readOnly = true)
public EmployeeDto getByCode(String code) {
return employeeRepository.findByCode(code).map(EmployeeDto::toDto)
.orElseThrow(() -> new EmployeeNotFoundException("Employee with " + code + " code not found!"));
}
@Override
@Transactional(readOnly = true)
public void auth(String code) {
if (employeeRepository.findByCode(code).isEmpty()) {
throw new EmployeeNotFoundException("Employee with " + code + " code not found!");
}
}
}

View File

@ -0,0 +1,22 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
h2:
console:
enabled: true
jpa:
generate-ddl: false
hibernate:
ddl-auto: none
show-sql: true
liquibase:
enabled: true
change-log: classpath:db.changelog/db.changelog-master.xml
booking:
days-ahead: 3

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="2025-11-05--0001-employee" author="anepretimov">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="employee"/>
</not>
</preConditions>
<createTable tableName="employee">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="code" type="VARCHAR(100)">
<constraints nullable="false" unique="true"/>
</column>
<column name="photo_url" type="VARCHAR(100)"/>
</createTable>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="2025-11-05--0002-place" author="anepretimov">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="place"/>
</not>
</preConditions>
<createTable tableName="place">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="place_name" type="VARCHAR(100)">
<constraints nullable="false" unique="true"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="2025-11-05--0003-booking" author="anepretimov">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="booking"/>
</not>
</preConditions>
<createTable tableName="booking">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="employee_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_booking_employee" referencedTableName="employee"
referencedColumnNames="id"/>
</column>
<column name="place_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_booking_place" referencedTableName="place"
referencedColumnNames="id"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="2025-11-05--0001-employee-data" author="anepretimov">
<loadData tableName="employee" file="db.changelog/data/csv/2025-11-05--0001-employee-data.csv"
separator=";"
quotchar='"'
encoding="UTF-8"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="2025-11-05--0002-place-data" author="anepretimov">
<loadData tableName="place" file="db.changelog/data/csv/2025-11-05--0002-place-data.csv"
separator=";"
quotchar='"'
encoding="UTF-8"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="2025-11-05--0003-booking-data" author="anepretimov">
<loadData tableName="booking" file="db.changelog/data/csv/2025-11-05--0003-booking-data.csv"
separator=";"
quotchar='"'
encoding="UTF-8"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,5 @@
name;code;photo_url
Ivanov Ivan;1111;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
Petrov Petr;2222;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
Kozlov Oleg;3333;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
Smirnova Anna;4444;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
1 name code photo_url
2 Ivanov Ivan 1111 https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
3 Petrov Petr 2222 https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
4 Kozlov Oleg 3333 https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
5 Smirnova Anna 4444 https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg

View File

@ -0,0 +1,4 @@
place_name
K-19
M-16
T-1
1 place_name
2 K-19
3 M-16
4 T-1

View File

@ -0,0 +1,3 @@
date;place_id;employee_id
2025-11-08;1;1
2025-11-10;2;2
1 date place_id employee_id
2 2025-11-08 1 1
3 2025-11-10 2 2

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
<include file="db.changelog/1/0/2025-11-05--0001-employee.xml"/>
<include file="db.changelog/1/0/2025-11-05--0002-place.xml"/>
<include file="db.changelog/1/0/2025-11-05--0003-booking.xml"/>
<include file="db.changelog/data/2025-11-05--0001-employee-data.xml"/>
<include file="db.changelog/data/2025-11-05--0002-place-data.xml"/>
<include file="db.changelog/data/2025-11-05--0003-booking-data.xml"/>
</databaseChangeLog>