init: added files of the second stage

This commit is contained in:
Petr Rudichev 2025-02-18 18:20:20 +03:00
commit cae71febb8
21 changed files with 568 additions and 0 deletions

32
.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

57
pom.xml Normal file
View File

@ -0,0 +1,57 @@
<?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-2024</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</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.4.0</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.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</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>
</dependencies>
</project>

81
readme.md Normal file
View File

@ -0,0 +1,81 @@
# НТО 2024. II отборочный этап. Командные задания — серверная часть
## 📖 Предыстория
В компании S контроль доступа в офис осуществляется с помощью СКУД (системы контроля управления доступом). На данный момент у каждого сотрудника компании есть карта-пропуск с NFC меткой. А у каждой входной двери есть считыватель с обеих сторон. При поднесении карты к считывателю, дверь открывается, а информация о времени входа или выхода сотрудника фиксируется в базе данных.
Администрации компании S требуется мобильное приложение, как для рядовых сотрудников, так и для администрации с возможностью просмотра посещений и работой электронного пропуска как временной замены обычного (при помощи сканировании QR кода, который находится на считывателе). Поскольку в приложении есть возможность использовать телефон как пропуск - то к данному приложению повышенные требования к безопасности всех данных, находящихся внутри него.
## 🛠️ Техническое задание
Требуется разработать серверное приложение на Java (Java 11) с использованием Spring Boot, которое работает на основе протоколов TCP/IP и взаимодействует с клиентами благодаря RESTful API. Для хранения данных о сотрудниках и их посещениях должна использоваться реляционная база данных (H2). Сотрудникам не нужно регистрироваться, все данные в базе должны быть предзаполнены. Картинки для аватаров пользователей не должны храниться в БД. Должны быть сохранены лишь URL-адреса на ресурсы, откуда в последующем мобильное приложение загрузит соответствующий аватар.
Сервер разрабатывается на основе предоставляемой заготовки проекта. Версии зависимостей и сами зависимости изменяться не должны.
## 📂 Правила работы с проектом-заготовкой
В предоставленном проекте необходимо изучить, но никак не модифицировать, не перемещать и не удалять следующие файлы:
- `pom.xml`
- `application.yml`
- все файлы из `db.changelog`
Кроме описанных выше файлов, в проекте уже созданы основные классы-сущности и добавлены пустые классы всех слоев. В этих классах необходимо будет написать программный код и добавить аннотации для реализации описанного задания. Наименования классов и прочий код уже написанный в предоставляемом проекте **изменять/удалять не нужно, необходимо их доработать**. Добавлять свои дополнительные классы в проект можно.
Создание таблиц в БД и предзаполнение их требуемыми данными уже реализовано в заготовке при помощи liquibase.
## 🌐 Где необходимо разместить сервер
Серверное приложение должно быть разработано и протестировано локально (не требуется размещать сервер удаленно и осуществлять его функционирование 24/7).
## 📋 Технические требования к серверу и его ответам клиенту
Для конфигурирования Вашего сервера (его хоста/IP адреса) используйте константы из файла `Constants.kt`.
| **Тип запроса** | **Путь** | **Параметры/Тело** | **Ответы** |
|------------------|-----------------------|--------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
| **GET** | `api/<LOGIN>/auth` | `<LOGIN>` - никнейм/имя пользователя | `400` - что-то пошло не так<br>`401` - логина не существует или неверный<br>`200` - данный логин существует - можно пользоваться приложением |
| **GET** | `api/<LOGIN>/info` | `<LOGIN>` - никнейм/имя пользователя | `400` - что-то пошло не так<br>`401` - логина не существует или неверный<br>`200` - ОК<br>{<br> "id": 1,<br> "login": "pivanov",<br> "name": "Иванов Петр Федорович",<br> "photo": "https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg",<br> "position": "Разработчик",<br> "lastVisit": "2024-02-12T08:30:00"<br>} |
| **PATCH** | `api/<LOGIN>/open` | `<LOGIN>` - никнейм/имя пользователя<br>Тело: `{“value”: <CODE>}`, где <br> `<CODE>` - это код, полученный из экрана сканирования QR кода | `400` - что-то пошло не так<br>`401` - логина не существует или неверный<br>`200` - дверь открылась |
## 📊 Данные, которыми необходимо наполнить базу данных:
| **id** | **login** | **name** | **photo** | **position** | **lastVisit** |
|--------|------------|---------------------------------|---------------------------------------------------------------------------------------------|------------------|---------------------|
| 1 | pivanov | Иванов Петр Федорович | https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg | Разработчик | 2024-02-12T08:30:00 |
| 2 | ipetrov | Петров Иван Константинович | https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg | Аналитик | 2024-02-30T08:35:00 |
| 3 | asemenov | Семенов Анатолий Анатольевич | https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg | Разработчик | 2024-02-31T08:31:00 |
| 4 | afedorov | Федоров Александр Сергеевич | https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg | Тестировщик | 2024-02-30T08:36:00 |
| **id** | **code** |
|--------|-------------------------|
| 1 | 1234567890123456789 |
| 2 | 9223372036854775807 |
| 3 | 1122334455667788990 |
| 4 | 998877665544332211 |
| 5 | 5566778899001122334 |
## 📝 Решение
Необходимо загрузить свое решение в систему [по ссылке](https://innovationcampus.ru/lms/mod/quiz/view.php?id=2078).
Отметим, что работу необходимо осуществлять в представленных проектах-заготовках (шаблонах).
## ✅ Особенности оценивания
Оценивание происходит с помощью автоматической системы тестирования, которая в автоматическом режиме находит элементы и взаимодействует с ними (именно для этого у каждого элемента указан уникальный идентификатор, по которому будет производится поиск). Каждый тест происходит с чистой установки приложения. В случае тестирования сервера на него поочередно отправляются команды, описанные в API и ожидаются определенные корректные ответы. Сервер и приложение тестируются независимо.

View File

@ -0,0 +1,29 @@
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) {
/*
Немножечко о том, как заставить это работать.
1) Зайдите в File (Панель наверху) -> Project Structure -> Перед вами будет поле SDK,
кликайте на него и ищите Install JDK..., нажимайте на него -> Выберите 11 версию и
Vendor: Amazon Corenda -> Устанавливайте.
2) Зайдите в Project Structure и выберите там установленный SDK.
3) Зайдите в настройки -> Build, Execution, Deployment -> Build Tools -> Maven ->
теперь поочереди зайдите во вкладки Importing, Runner и выберите там установленную SDK.
4) Теперь тыкните на зеленый треугольник сбоку. IDE попробует запустить проект, но, вероятно,
у неё не получится.
После неудачной попытки сверху появится кнопка с надписью App, тыкаем. Переходим в
Edit Configurations..., и там возле надписи Build and Run, выбираем установленную SDK.
5) Пробуем запустить!
По идее гайд должен быть рабочим, но я не уверен...
*/
SpringApplication.run(App.class, args);
}
}

View File

@ -0,0 +1,54 @@
package com.example.nto.aspect;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect
@Component
public class LoggingAspect {
// Это класс логирования. Здесь находятся методы, отвечающие за вывод дополнительной информации в консоль.
// Чтобы понять, что здесь происходит надо посмотреть >>>
// > https://disk.yandex.ru/i/E34JfcAK8nA4Vw
// > https://disk.yandex.ru/i/9TNQ8D5V3cPjrg
// Главный класс, поставляется вместе с `spring-boot-starter-web`.
// ! Библиотека называется log4j !
private static final Logger logger = LogManager.getLogger(LoggingAspect.class);
// Аннотация, объединяющая @Before и @AfterReturning.
// Тут мы говорим, какая аннотация будет запускать процесс логирования в методе.
// Сюда также можно передать путь к классу, чтобы все методы из него логировались, но делать так не надо.
//
@Around("@annotation (com.example.nto.aspect.annotation.LogExample)")
// Это что-то типа представления метода, тут хранятся название, аргументы, возвращаемое значение и тд.
//
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
logger.info(
" >>> Логирование метода: {} | Входные параметры: {} <<<",
joinPoint.getSignature().getName(), // Получение названия метода.
Arrays.toString(joinPoint.getArgs()) // Получение аргументов, передаваемы в метод.
);
Object result = joinPoint.proceed(); // Получает результат выполнения функции.
logger.info(" >>> Завершил работу метод: {} | Результат работы: {} <<<",
joinPoint.getSignature().getName(), result);
return result;
}
// Эта штука должна иметь такое же название, как и аргумент `Object` снизу.
//
@AfterThrowing(pointcut = "@annotation (com.example.nto.aspect.annotation.LogExample)", throwing = "error")
public void logAfterThrowing(JoinPoint joinPoint, Object error) {
logger.error(error.toString());
}
}

View File

@ -0,0 +1,11 @@
package com.example.nto.aspect.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExample { }
// Про это говорится ~16 минуте - https://disk.yandex.ru/i/9TNQ8D5V3cPjrg

View File

@ -0,0 +1,40 @@
package com.example.nto.controller;
import com.example.nto.entity.Code;
import com.example.nto.entity.Employee;
import com.example.nto.service.CodeService;
import com.example.nto.service.EmployeeService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class EmployeeController {
private final EmployeeService employeeService;
private final CodeService codeService;
@GetMapping("/{login}/auth")
@ResponseStatus(HttpStatus.OK) // Возвращаемое по умолчанию значение.
public void verificationLogin(@PathVariable String login) {
employeeService.get(login);
}
@GetMapping("/{login}/info")
@ResponseStatus(HttpStatus.OK)
public Employee getEmployee(@PathVariable String login) {
return employeeService.get(login);
}
@PatchMapping("/{login}/open")
@ResponseStatus(HttpStatus.OK)
public void visit(@PathVariable String login, @RequestBody Code code) {
codeService.getCode(code);
Employee employee = employeeService.get(login);
employeeService.update(employee, LocalDateTime.now());
}
}

View File

@ -0,0 +1,21 @@
package com.example.nto.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Data // Генерирует геттеры, сеттеры и тд.
@Entity // Говорит Spring, что это сущность из базы данных.
@Builder // Добавляет возможность использовать builder-стиль создания класса. ( Color.builder().setGreen(...).setBlue(...).build() )
@NoArgsConstructor // Добавляет конструктор без аргументов.
@AllArgsConstructor // Добавляет конструктор со всеми аргументами.
@Table(name = "code")
public class Code {
@Id
private long value;
}

View File

@ -0,0 +1,27 @@
package com.example.nto.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.time.LocalDateTime;
@Data // Генерирует геттеры, сеттеры и тд.
@Entity // Говорит Spring, что это сущность из базы данных.
@Builder // Добавляет возможность использовать builder-стиль создания класса. ( Color.builder().setGreen(...).setBlue(...).build() )
@NoArgsConstructor // Добавляет конструктор без аргументов.
@AllArgsConstructor // Добавляет конструктор со всеми аргументами.
@Table(name = "employee")
public class Employee {
@Id
private long id;
private String login;
private String name;
private String photo;
private String position;
private LocalDateTime lastVisit;
}

View File

@ -0,0 +1,7 @@
package com.example.nto.exception;
public class CodeNotFoundException extends RuntimeException {
// Выдаётся, когда код не найден. Нужна, чтобы возвращать клиенту код 401.
public CodeNotFoundException(String message) { super(message); }
}

View File

@ -0,0 +1,7 @@
package com.example.nto.exception;
public class EmployeeNotFoundException extends RuntimeException {
// Выдаётся, когда работник не найден. Нужна, чтобы возвращать клиенту код 401.
public EmployeeNotFoundException(String message) { super(message); }
}

View File

@ -0,0 +1,7 @@
package com.example.nto.exception;
public class SomethingWentWrongException extends RuntimeException {
// Выдаётся, при непредвиденной ошибке. Нужна, чтобы возвращать клиенту код 400.
public SomethingWentWrongException(String message) { super(message); }
}

View File

@ -0,0 +1,30 @@
package com.example.nto.exception.advice;
import com.example.nto.exception.CodeNotFoundException;
import com.example.nto.exception.EmployeeNotFoundException;
import com.example.nto.exception.SomethingWentWrongException;
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 GlobalExceptionalHandler {
// При выбрасывании исключений EmployeeNotFoundException и CodeNotFoundException возвращает клиенту код 401.
// При выбрасывании исключения SomethingWentWrongException возвращает 400.
@ExceptionHandler(EmployeeNotFoundException.class)
public ResponseEntity<HttpStatus> handlerEmployeeNotFoundException(EmployeeNotFoundException e) {
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
@ExceptionHandler(CodeNotFoundException.class)
public ResponseEntity<HttpStatus> handlerCodeNotFoundException(CodeNotFoundException e) {
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
@ExceptionHandler(SomethingWentWrongException.class)
public ResponseEntity<HttpStatus> handlerSomethingWentWrongException(SomethingWentWrongException e) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
package com.example.nto.service;
import com.example.nto.entity.Code;
public interface CodeService {
Code getCode(Code code);
}

View File

@ -0,0 +1,10 @@
package com.example.nto.service;
import com.example.nto.entity.Employee;
import java.time.LocalDateTime;
public interface EmployeeService {
Employee get(String login);
Employee update(Employee employee, LocalDateTime last_visit);
}

View File

@ -0,0 +1,39 @@
package com.example.nto.service.impl;
import com.example.nto.aspect.annotation.LogExample;
import com.example.nto.entity.Code;
import com.example.nto.exception.SomethingWentWrongException;
import com.example.nto.repository.CodeRepository;
import com.example.nto.service.CodeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CodeServiceImpl implements CodeService {
// Класс отвечает за основную логику приложения. Тут мы получаем из бд и обработав их возвращаем клиенту.
private final CodeRepository codeRepository; // Это объект для работы с бд.
@LogExample
@Override
public Code getCode(Code code) {
// В ТЗ написано `401 - логина не существует или неверный`.
// Вообще тут спорный момент, надо ли возвращать 401, если `Code` не существует.
// Так что напишу здесь два варианта, авось какой-нибудь прокатит.
// final String error_message = "QR-код с кодом " + code.getValue() + " не найден!";
//
// try {
// codeRepository.findById(code.getValue()).orElseThrow(() -> new CodeNotFoundException(error_message));
// return code;
// }
// catch (CodeNotFoundException e) { throw new CodeNotFoundException(e.getMessage()); }
// catch (Exception e) { throw new SomethingWentWrongException("Непредвиденная ошибка"); }
// Проверка, существует ли `Code`. Если не существует, вернётся ошибка 400.
codeRepository.findById(code.getValue()).orElseThrow(() -> new SomethingWentWrongException("Непредвиденная ошибка"));
return code;
}
}

View File

@ -0,0 +1,53 @@
package com.example.nto.service.impl;
import com.example.nto.aspect.annotation.LogExample;
import com.example.nto.entity.Employee;
import com.example.nto.exception.EmployeeNotFoundException;
import com.example.nto.exception.SomethingWentWrongException;
import com.example.nto.repository.EmployeeRepository;
import com.example.nto.service.EmployeeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Objects;
@Service
@RequiredArgsConstructor
public class EmployeeServiceImpl implements EmployeeService {
// Класс отвечает за основную логику приложения. Тут мы получаем из бд и обработав их возвращаем клиенту.
private final EmployeeRepository employeeRepository; // Это объект для работы с бд.
@Override
@LogExample
public Employee get(String login) {
final String error_message = "Работник с логином " + login + " не найден!";
try {
return employeeRepository.findAll().stream()
.filter(e -> Objects.equals(e.getLogin(), login)).findFirst()
.orElseThrow(() -> new EmployeeNotFoundException(error_message));
}
catch (EmployeeNotFoundException e) { throw new EmployeeNotFoundException(e.getMessage()); }
catch (Exception e) { throw new SomethingWentWrongException("Непредвиденная ошибка"); }
}
@Override
@LogExample
public Employee update(Employee employee, LocalDateTime last_visit) {
final String error_message = "Работник с логином " + employee.getLogin() + " не найден!";
try {
// Проверка, что работник существует.
employeeRepository.findById(employee.getId()).orElseThrow(() -> new EmployeeNotFoundException(error_message));
employee.setLastVisit(last_visit);
employeeRepository.save(employee);
return employee;
}
catch (EmployeeNotFoundException e) { throw new EmployeeNotFoundException(e.getMessage()); }
catch (Exception e) { throw new SomethingWentWrongException("Непредвиденная ошибка"); }
}
}

View File

@ -0,0 +1,28 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
h2:
console:
#enabled: false
enabled: true
jpa:
#generate-ddl: false
generate-ddl: true
hibernate:
#ddl-auto: none
ddl-auto: create-drop
# Показываем запросы
show-sql: true
# Своевременный запуск data.sql
defer-datasource-initialization: true
spring-doc:
swagger-ui:
path: /swagger-ui.html
operationsSorter: method

View File

@ -0,0 +1,14 @@
INSERT INTO employee (id, login, name, photo, position, last_visit)
VALUES
(1, 'pivanov', 'Иванов Петр Федорович', 'https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg', 'Разработчик', '2024-02-12T08:30'),
(2, 'ipetrov', 'Петров Иван Константинович', 'https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg', 'Аналитик', '2024-02-13T08:35'),
(3, 'asemenov', 'Семенов Анатолий Анатольевич', 'https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg', 'Разработчик', '2024-02-13T08:31'),
(4, 'afedorov', 'Федоров Александр Сергеевич', 'https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg', 'Тестировщик', '2024-02-12T08:36');
INSERT INTO code (value)
VALUES
(1234567890123456789),
(9223372036854775807),
(1122334455667788990),
(998877665544332211),
(5566778899001122334);