results of the first day

Reviewed-on: #1
This commit is contained in:
Petr 2025-02-18 17:35:57 +00:00
commit 9c5bf336ec
28 changed files with 617 additions and 7 deletions

View File

@ -1,5 +1,10 @@
# НТО 2024. II отборочный этап. Командные задания — серверная часть
## Схема базы данных
https://www.drawdb.app/editor?shareId=92ab675631181485a028270c35276710
[Архитектура базы данных](static/bd.png)
## 📖 Предыстория
В компании S контроль доступа в офис осуществляется с помощью СКУД (системы контроля управления доступом). На данный момент у каждого сотрудника компании есть карта-пропуск с NFC меткой. А у каждой входной двери есть считыватель с обеих сторон. При поднесении карты к считывателю, дверь открывается, а информация о времени входа или выхода сотрудника фиксируется в базе данных.

View File

@ -0,0 +1,122 @@
package com.example.nto.domain.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "employees")
public class Employee implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name")
@NotBlank(message = "Имя пользователя не может быть пустым!")
@Size(max = 100, message = "Максимальная длина имени 100 символов!")
private String name;
@Column(name = "surname")
@NotBlank(message = "Фамилия пользователя не может быть пустой!")
@Size(max = 100, message = "Максимальная длина фамилии 100 символов!")
private String surname;
@Column(name = "patronymic")
@Size(max = 100, message = "Максимальная длина отчества 100 символов!")
private String patronymic;
@Column(name = "about_me")
@Size(max = 300, message = "Максимальная длина поля 'О себе' 300 символов!")
private String aboutMe;
@Column(name = "telephone", unique = true)
@NotBlank(message = "Телефон не может быть пустым!")
@Size(max = 20, message = "Максимальная длина телефонного номера 20 символов!")
private String telephone;
@Column(name = "email", unique = true)
@NotBlank(message = "Email не может быть пустым!")
@Size(max = 255, message = "Максимальная длина email 255 символов")
@Email(message = "Email адрес должен быть в формате user@example.com!")
private String email;
@Column(name = "password")
@NotBlank(message = "Пароль не может быть пустым!")
@Size(max = 300, message = "Максимальная длина пароля 300 символов!")
private String password;
// @ManyToOne(fetch = FetchType.EAGER)
// @JoinColumn(name = "office_id", referencedColumnName = "id", nullable = false)
// private Office office;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "pos_id", referencedColumnName = "id", nullable = false)
private Position position;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "role_id", referencedColumnName = "id", nullable = false)
private Role role;
@Column(name = "profile_image_url", nullable = false)
@Size(max = 300, message = "Максимальная длина адреса изображения 300 символов!")
private String profileImageUrl;
@CreatedDate
@Column(name = "created_at", columnDefinition = "TIMESTAMP", nullable = false)
private LocalDateTime createdAt;
// @OneToMany(mappedBy = "employee")
// private List<Visit> visits = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(this.role);
}
@Override
public String getPassword() {
return "";
}
@Override
public String getUsername() {
return this.email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}

View File

@ -0,0 +1,68 @@
package com.example.nto.domain.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "offices")
public class Office {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name", unique = true)
@NotBlank(message = "Название не может быть пустым!")
@Size(max = 100, message = "Максимальная длина названия 100 символов!")
private String name;
@Column(name = "description")
@NotBlank(message = "Описание не может быть пустым!")
@Size(max = 300, message = "Максимальная длина описания 300 символов!")
private String description;
@Column(name = "address")
@NotBlank(message = "Адрес не может быть пустым!")
@Size(max = 200, message = "Максимальный размер адреса 200 символов!")
private String address;
@Column(name = "latitude")
@NotNull(message = "Широта не может быть пустой!")
private Double latitude;
@Column(name = "longitude")
@NotNull(message = "Долгота не может быть пустой!")
private Double longitude;
@Column(name = "logo_image_url")
@NotBlank(message = "Путь к логотипу не может быть пустой!")
@Size(max = 200, message = "Максимальный размер пути к логотипу 200 символов!")
private String linkLogo;
@Column(name = "telephone")
@Size(max = 20, message = "Максимальная длина телефонного номера 20 символов!")
private String telephone;
@Column(name = "email")
@Size(max = 255, message = "Максимальная длина email 255 символов")
@Email(message = "Email адрес должен быть в формате user@example.com!")
private String email;
@OneToMany(mappedBy = "office")
private List<Employee> employeeList;
@OneToMany(mappedBy = "office")
private List<Terminal> terminals;
}

View File

@ -0,0 +1,31 @@
package com.example.nto.domain.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "positions")
public class Position {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name", unique = true)
@NotBlank(message = "Название не может быть пустым!")
@Size(max = 100, message = "Максимальная длина названия 100 символов!")
private String name;
@OneToMany(mappedBy = "position")
private List<Employee> employees;
}

View File

@ -0,0 +1,37 @@
package com.example.nto.domain.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import java.util.List;
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "roles")
public class Role implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Column(name = "role_name", unique = true)
@NotBlank(message = "Название роли не может быть пустой!")
@Size(max = 100, message = "Максимальная длина роли 100 символов!")
private String roleName;
@OneToMany(mappedBy = "role")
private List<Employee> employees;
@Override
public String getAuthority() {
return this.roleName;
}
}

View File

@ -0,0 +1,48 @@
package com.example.nto.domain.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.UuidGenerator;
import java.util.List;
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "terminals")
public class Terminal {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "name")
@NotBlank(message = "Название не может быть пустым!")
@Size(max = 100, message = "Максимальная длина названия 100 символов!")
private String name;
// Мне нужна была рандомная генерация кода для терминала, так что я мог сделать кривую реализацию через самописную функцию,
// но боялся, что она будет генерировать не уникальные значения. (я очень смутно представляю, как эта штука работает)
// Код взят отсюда: https://stackoverflow.com/questions/76723290/using-the-new-type-for-uuidgenerator-instead-of-strategy
@UuidGenerator
@NotBlank(message = "Код не может быть пустым!")
@Column(name = "code", nullable = false, unique = true)
private String code;
// todo: Протестировать нужен ли тут каскад тип
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
@JoinColumn(name = "office_id", nullable = false)
private Office office;
@OneToMany(mappedBy = "startTerminal")
private List<Visit> startVisits;
@OneToMany(mappedBy = "endTerminal")
private List<Visit> endVisits;
}

View File

@ -0,0 +1,43 @@
package com.example.nto.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "visits")
public class Visit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "employee_id", referencedColumnName = "id", nullable = false)
private Employee employee;
@Column(name = "start_visit", columnDefinition = "TIMESTAMP", nullable = false)
private LocalDateTime startVisit;
@Column(name = "end_visit", columnDefinition = "TIMESTAMP")
private LocalDateTime endVisit;
@Column(name = "is_finished", nullable = false)
private boolean isFinished = false;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "start_terminal_id", referencedColumnName = "code", nullable = false)
private Terminal startTerminal;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "end_terminal_id", referencedColumnName = "code")
private Terminal endTerminal;
}

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
package com.example.nto.exception.advice;
package com.example.nto.domain.exception.advice;
import com.example.nto.exception.CodeNotFoundException;
import com.example.nto.exception.EmployeeNotFoundException;
import com.example.nto.exception.SomethingWentWrongException;
import com.example.nto.domain.exception.CodeNotFoundException;
import com.example.nto.domain.exception.EmployeeNotFoundException;
import com.example.nto.domain.exception.SomethingWentWrongException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;

View File

@ -0,0 +1,14 @@
package com.example.nto.dto.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OfficeDTO {
}

View File

@ -0,0 +1,22 @@
package com.example.nto.dto.entity;
import com.example.nto.dto.entity.employee.EmployeeItemDTO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PositionDTO {
private long id;
private String name;
// Список всех сотрудников с этой должностью.
private List<EmployeeItemDTO> employeeItemDTOList;
}

View File

@ -0,0 +1,23 @@
package com.example.nto.dto.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TerminalDTO {
private long id;
private String name;
// ОЧЕНЬ ВАЖНО!!! При создании терминала code не нужен, но отправлять его нужно.
// При создании пиши вместо code, что хочешь он будет просто игнорироваться.
// Мне просто очень лень делать отдельный TerminalCreateDTO ради одного поля.
private String code;
private String officeName;
}

View File

@ -0,0 +1,27 @@
package com.example.nto.dto.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VisitDTO {
private long id;
// Возвращается время начала и конца посещения в формате LocalDateTime.toString(),
// превратить обратно можно с помощью LocalDateTime.parse().
private String startVisit;
private String endVisit;
private boolean isFinished;
// Возвращается длительность посещения в формате LocalDateTime.toString(),
// превратить обратно можно с помощью LocalDateTime.parse().
private String durationVisit;
private String officeName;
}

View File

@ -0,0 +1,13 @@
package com.example.nto.dto.entity.employee;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeCreateDTO {
}

View File

@ -0,0 +1,51 @@
package com.example.nto.dto.entity.employee;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeDTO {
private long id;
private String name;
private String surname;
private String patronymic;
private String telephone;
private String email;
private long officeId;
private String officeName;
private String officeImageUrl;
private String position; // Название должности
private String role; // строка либо ROLE_USER, либо ROLE_ADMIN
private String profileImageUrl;
// Текущее состояние входа: false - visit (посещение) ещё не началось, true - visit идёт
private boolean visitStatus;
// Если visitStatus true, то возвращает дату и время начала посещения в формате LocalDateTime.toString(),
// превратить обратно можно с помощью LocalDateTime.parse().
// Если visitStatus false, то возвращает null.
private String startVisitDateTime;
private List<Long> visitsIdLast30Days; // Список Id посещений за последние 30 дней.
// Возвращает количество отработанных часов за последний месяц.
private long totalTimeVisitsLast30Days;
// (Возможно это стоит убрать) Название офиса, в котором сейчас находится работник.
// Если visitStatus false, возвращает null.
private String currentOfficeName;
// Возвращает время регистрации в формате LocalDateTime.toString(),
// превратить обратно можно с помощью LocalDateTime.parse().
private String createAt;
}

View File

@ -0,0 +1,14 @@
package com.example.nto.dto.entity.employee;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeItemDTO {
}

View File

@ -0,0 +1,4 @@
package com.example.nto.dto.mappers;
public class OfficeMapper {
}

View File

@ -0,0 +1,4 @@
package com.example.nto.dto.mappers;
public class PositionMapper {
}

View File

@ -0,0 +1,4 @@
package com.example.nto.dto.mappers;
public class TerminalMapper {
}

View File

@ -0,0 +1,4 @@
package com.example.nto.dto.mappers;
public class VisitMapper {
}

View File

@ -0,0 +1,4 @@
package com.example.nto.dto.mappers.employee;
public class EmployeeCreateMapper {
}

View File

@ -0,0 +1,4 @@
package com.example.nto.dto.mappers.employee;
public class EmployeeItemMapper {
}

View File

@ -0,0 +1,4 @@
package com.example.nto.dto.mappers.employee;
public class EmployeeMapper {
}

View File

@ -0,0 +1,5 @@
package com.example.nto.utils;
public enum DataFormatType {
DATE_TIME, DATE, TIME
}

View File

@ -0,0 +1,59 @@
package com.example.nto.utils;
import lombok.experimental.UtilityClass;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@UtilityClass
public class Utils {
private static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
public static String profileFileName(long userId) {
return "profile--" + userId;
}
public static String generateUniqueName() {
return UUID.randomUUID().toString();
}
public static String nowTime(DataFormatType type) {
switch (type) {
case DATE_TIME: return LocalDateTime.now().format(DATE_TIME_FORMAT);
case DATE: return LocalDateTime.now().format(DATE_FORMAT);
case TIME: return LocalDateTime.now().format(TIME_FORMAT);
default: return "Произошла ошибка при форматировании даты или времени.";
}
}
public static LocalDateTime period(LocalDateTime dtStart, LocalDateTime dtEnd) {
// Возвращает разницу между двумя LocalDateTime
Period period = Period.between(dtStart.toLocalDate(), dtEnd.toLocalDate());
Duration duration = Duration.between(dtStart.toLocalTime(), dtEnd.toLocalTime());
LocalDate localDate = LocalDate.of(period.getYears(), period.getMonths(), period.getDays());
LocalTime localTime = LocalTime.of(duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart());
return LocalDateTime.of(localDate, localTime);
}
public static long periods(List<List<LocalDateTime>> periods) {
// Количество часов за определенные периоды.
long hours = 0;
for (List<LocalDateTime> period : periods) {
if (period.size() != 2) throw new IllegalStateException("Список с периодом должен содержать 2 элемента!");
hours += Duration.between(period.get(0), period.get(1)).toHours();
}
return hours;
}
public static String convertDistance(float distance) {
if (distance > 1000) return String.format("%.1f", distance / 1000) + " км";
else return String.format("%.1f", distance) + " м";
}
}

BIN
static/bd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB