the first working version #2

Merged
Petr merged 15 commits from develop into master 2025-02-19 07:56:27 +00:00
51 changed files with 332 additions and 69 deletions

View File

@ -2,7 +2,7 @@
## Схема базы данных
https://www.drawdb.app/editor?shareId=92ab675631181485a028270c35276710
[Архитектура базы данных](static/bd.png)
[Архитектура базы данных](docs/static/bd.png)
## 📖 Предыстория

BIN
docs/static/bd.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
images/profile--1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
images/profile--10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
images/profile--11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
images/profile--12.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
images/profile--13.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
images/profile--14.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
images/profile--15.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
images/profile--16.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
images/profile--17.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
images/profile--18.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
images/profile--19.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
images/profile--2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
images/profile--20.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
images/profile--21.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
images/profile--22.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
images/profile--23.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
images/profile--24.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
images/profile--25.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
images/profile--26.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
images/profile--3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
images/profile--4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
images/profile--5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
images/profile--6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
images/profile--7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
images/profile--8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
images/profile--9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -22,7 +22,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<version>2.7.10</version>
</parent>
<dependencies>

View File

@ -0,0 +1,42 @@
package com.example.nto.config;
import com.amazonaws.SdkClientException;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Getter
@Configuration
public class ObjectStorageConfig {
@Value("${yandex.cloud.region}")
private String region;
@Value("${yandex.cloud.s3_endpoint}")
private String s3Endpoint;
@Value("${yandex.cloud.bucket}")
private String bucketName;
@Value("${yandex.cloud.key_id}")
private String accessKeyId;
@Value("${yandex.cloud.secret_key}")
private String secretAccessKey;
public AmazonS3 createAmazonS3() {
// Создание клиента AmazonS3 с подключением к Object Storage
try {
return AmazonS3ClientBuilder.standard()
.withEndpointConfiguration(new com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration(s3Endpoint, region))
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKeyId, secretAccessKey)))
.build();
} catch (SdkClientException e) {
log.error("Ошибка при создании клиента для хранения объектов с помощью AWS SDK. Причина: {}", e.getMessage());
throw new SdkClientException(e.getMessage());
}
}
}

View File

@ -0,0 +1,20 @@
package com.example.nto.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@Configuration
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.preparation"))
.paths(PathSelectors.any())
.build();
}
}

View File

@ -0,0 +1,53 @@
package com.example.nto.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/h2-console/**").permitAll()
.antMatchers("/api/v1/images/**").permitAll()
.antMatchers("/api/v1/volunteers/login").permitAll()
.antMatchers("/api/v1/volunteers/register").permitAll()
.antMatchers("/api/v1/**").permitAll()
//.antMatchers("/api/v1/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.headers().frameOptions().disable();
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/api/v1/volunteers/images");
}
}

View File

@ -1,9 +1,6 @@
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;
@ -12,8 +9,11 @@ import org.springframework.data.annotation.CreatedDate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@ -62,9 +62,9 @@ public class Employee implements UserDetails {
@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 = "office_id", referencedColumnName = "id", nullable = false)
private Office office;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "pos_id", referencedColumnName = "id", nullable = false)
@ -78,12 +78,15 @@ public class Employee implements UserDetails {
@Size(max = 300, message = "Максимальная длина адреса изображения 300 символов!")
private String profileImageUrl;
@Column(name = "is_blocked")
private boolean isBlocked;
@CreatedDate
@Column(name = "created_at", columnDefinition = "TIMESTAMP", nullable = false)
private LocalDateTime createdAt;
// @OneToMany(mappedBy = "employee")
// private List<Visit> visits = new ArrayList<>();
@OneToMany(mappedBy = "employee")
private List<Visit> visits;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
@ -92,7 +95,7 @@ public class Employee implements UserDetails {
@Override
public String getPassword() {
return "";
return this.password;
}
@Override

View File

@ -1,15 +1,16 @@
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 javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;
@Data

View File

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

View File

@ -1,13 +1,14 @@
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 javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.util.List;
@Data

View File

@ -1,14 +1,15 @@
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 javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.util.List;
@Data

View File

@ -1,14 +1,15 @@
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 org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.util.List;
@Data
@ -29,8 +30,9 @@ public class Terminal {
// Мне нужна была рандомная генерация кода для терминала, так что я мог сделать кривую реализацию через самописную функцию,
// но боялся, что она будет генерировать не уникальные значения. (я очень смутно представляю, как эта штука работает)
// Код взят отсюда: https://stackoverflow.com/questions/76723290/using-the-new-type-for-uuidgenerator-instead-of-strategy
@UuidGenerator
// Код взят отсюда: https://stackoverflow.com/questions/25082244/auto-generate-unique-random-string-in-spring-mvc-hibernate
@GenericGenerator(name = "uuid-gen", strategy = "uuid")
@GeneratedValue(generator = "uuid-gen")
@NotBlank(message = "Код не может быть пустым!")
@Column(name = "code", nullable = false, unique = true)
private String code;

View File

@ -1,11 +1,11 @@
package com.example.nto.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@ -30,7 +30,7 @@ public class Visit {
private LocalDateTime endVisit;
@Column(name = "is_finished", nullable = false)
private boolean isFinished = false;
private final boolean isFinished = false;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "start_terminal_id", referencedColumnName = "code", nullable = false)
@ -39,5 +39,9 @@ public class Visit {
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "end_terminal_id", referencedColumnName = "code")
private Terminal endTerminal;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "type_passage", referencedColumnName = "id", nullable = false)
private Passage passage;
}

View File

@ -1,14 +1,29 @@
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 OfficeDTO {
private long id;
private String name;
private String description;
private String address;
private Double latitude;
private Double longitude;
private String linkLogo;
private String telephone;
private String email;
private List<EmployeeItemDTO> employees;
private List<TerminalDTO> terminals;
}

View File

@ -10,4 +10,17 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeCreateDTO {
private String name;
private String surname;
private String patronymic;
private String telephone;
private String email;
private String password;
private boolean isBlocked;
private String officeName; // Имя офиса, к которому присоединится работник.
private String positionName; // Должность работника.
private String role; // строка либо ROLE_USER, либо ROLE_ADMIN
// Изображение профиля выберется рандомно из заготовок.
}

View File

@ -27,6 +27,7 @@ public class EmployeeDTO {
private String role; // строка либо ROLE_USER, либо ROLE_ADMIN
private String profileImageUrl;
private boolean isBlocked;
// Текущее состояние входа: false - visit (посещение) ещё не началось, true - visit идёт
private boolean visitStatus;

View File

@ -11,4 +11,16 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeItemDTO {
private long id;
private String name;
private String surname;
private String patronymic;
private String profileImageUrl;
private boolean isBlocked;
private String officeName;
private String position; // Название должности
// Текущее состояние входа: false - visit (посещение) ещё не началось, true - visit идёт
private boolean visitStatus;
}

View File

@ -1,4 +1,32 @@
package com.example.nto.dto.mappers.employee;
import com.example.nto.domain.entity.Employee;
import com.example.nto.domain.entity.Office;
import com.example.nto.domain.entity.Position;
import com.example.nto.domain.entity.Role;
import com.example.nto.dto.entity.employee.EmployeeCreateDTO;
import com.example.nto.utils.Utils;
import lombok.experimental.UtilityClass;
@UtilityClass
public class EmployeeCreateMapper {
public static Employee convertFromDTO(
EmployeeCreateDTO employeeDTO, String password, Office office,
Position position, Role role
) {
Employee employee = new Employee();
employee.setName(employeeDTO.getName());
employee.setSurname(employeeDTO.getSurname());
employee.setPatronymic(employeeDTO.getPatronymic());
employee.setTelephone(employeeDTO.getTelephone());
employee.setEmail(employeeDTO.getEmail());
employee.setPassword(password);
employee.setOffice(office);
employee.setPosition(position);
employee.setRole(role);
employee.setProfileImageUrl(Utils.getRandomUrlProfileImage());
return employee;
}
}

View File

@ -0,0 +1,14 @@
package com.example.nto.service.impl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}

View File

@ -1,5 +1,7 @@
package com.example.nto.utils;
import com.example.nto.config.ObjectStorageConfig;
import com.example.nto.domain.entity.Visit;
import lombok.experimental.UtilityClass;
import java.time.*;
@ -7,6 +9,7 @@ import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
@UtilityClass
@ -15,6 +18,8 @@ public class Utils {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
private static final ObjectStorageConfig storageConfig = new ObjectStorageConfig();
public static String profileFileName(long userId) {
return "profile--" + userId;
}
@ -25,12 +30,17 @@ public class Utils {
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 "Произошла ошибка при форматировании даты или времени.";
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());
@ -52,6 +62,23 @@ public class Utils {
return hours;
}
public static List<Visit> filterDateLast30Days(List<Visit> visits) {
final LocalDateTime referenceDate = LocalDateTime.now();
List<Visit> result = new ArrayList<>();
for (Visit visit : visits) {
long daysBetween = ChronoUnit.DAYS.between(visit.getStartVisit(), referenceDate);
if (daysBetween >= 0 && daysBetween <= 30) result.add(visit);
}
return result;
}
public static String getRandomUrlProfileImage() {
int max = 26, min = 1;
String fileName = profileFileName(new Random().nextInt(max - min + 1) + min) + ".jpg";
return storageConfig.getS3Endpoint() + "/" + storageConfig.getBucketName() + "/standard/" + fileName;
}
public static String convertDistance(float distance) {
if (distance > 1000) return String.format("%.1f", distance / 1000) + " км";
else return String.format("%.1f", distance) + " м";

View File

@ -0,0 +1,7 @@
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
yandex.cloud.region=ru-central1
yandex.cloud.s3_endpoint=https://storage.yandexcloud.net
yandex.cloud.key_id=YCAJEIOy-pmwxonO9XLqdhAVI
yandex.cloud.secret_key=YCNJMJf9VGP9OmFiPs5XbnfKzzarBl1B1ymGz1uu
yandex.cloud.bucket=spring-boot-final-nto-bacet

View File

@ -1,28 +1,25 @@
server:
port: 8080
spring:
servlet:
multipart:
max-file-size: ${MAX_FILE_SIZE:8MB}
max-request-size: ${MAX_REQUEST_SIZE:8MB}
datasource:
url: jdbc:h2:mem:testdb
url: jdbc:h2:mem:database
h2:
console:
#enabled: false
enabled: true
liquibase:
enabled: true
change-log: classpath:db.changelog/db.changelog-master.xml
jpa:
#generate-ddl: false
generate-ddl: true
generate-ddl: false
hibernate:
#ddl-auto: none
ddl-auto: create-drop
# Показываем запросы
ddl-auto: none
show-sql: true
# Своевременный запуск data.sql
defer-datasource-initialization: true
spring-doc:
swagger-ui:
path: /swagger-ui.html
operationsSorter: method

View File

@ -1,14 +0,0 @@
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);

View File

@ -0,0 +1,4 @@
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd">
</databaseChangeLog>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB