Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d0f076a
(TP-71) feat: add image table init migration file
Kattsyn Apr 2, 2025
922ace7
(TP-71) feat: add image entity
Kattsyn Apr 2, 2025
b977648
(TP-71) feat: add image repository
Kattsyn Apr 2, 2025
4bda513
(TP-71) feat: add store service interface
Kattsyn Apr 2, 2025
c8047ac
(TP-71) feat: add in-memory storage service impl
Kattsyn Apr 2, 2025
e553682
(TP-71) feat: add env variable for upload root path
Kattsyn Apr 2, 2025
a3d422e
(TP-71) feat: property source annotation in main class
Kattsyn Apr 2, 2025
6254c50
(TP-71) feat: add image service interface
Kattsyn Apr 2, 2025
94546ec
(TP-71) feat: add image service impl
Kattsyn Apr 2, 2025
59c0188
(TP-71) feat: add image controller
Kattsyn Apr 2, 2025
410956e
(TP-71) feat: add enum ImageType
Kattsyn Apr 3, 2025
63f857e
(TP-71) feat: add PathResolver
Kattsyn Apr 3, 2025
a502c00
(TP-71) feat: add max-file-size in app.yml
Kattsyn Apr 3, 2025
520bce2
(TP-71) feat: change images_init version and add additional_path var
Kattsyn Apr 3, 2025
0a677bd
(TP-71) feat: change varchar size for description
Kattsyn Apr 3, 2025
2c347d8
(TP-71) feat: add reference for image in facility and category
Kattsyn Apr 3, 2025
860231f
(TP-71) feat: change StorageService interface and impl
Kattsyn Apr 3, 2025
039fdc2
(TP-71) feat: update description length and add images reference
Kattsyn Apr 3, 2025
50d682f
(TP-71) feat: add additionalPath
Kattsyn Apr 3, 2025
2a3137c
(TP-71) feat: change image service interface and impl
Kattsyn Apr 3, 2025
f624ca4
(TP-71) feat: add imageType parameter for uploadImage
Kattsyn Apr 3, 2025
c1aa6da
(TP-71) feat: add image reference and adding for categories
Kattsyn Apr 3, 2025
d25b39b
(TP-71) feat: override CategoryServiceImpl uploadImage method
Kattsyn Apr 3, 2025
e7a03c0
(TP-71) feat: add image reference and adding to facility module
Kattsyn Apr 3, 2025
94b04f9
(TP-71) feat: add image reference and adding to property module
Kattsyn Apr 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
# Backend
# Backend

## Запуск docker-compose

Находясь в корневой папке проекта:
1. `./gradlew build` - Для сборки проекта
2. `docker build -f Dockerfile .` - Для сборки Dockerfile
3. `docker compose build` - Для сборки docker-compose
4. `docker compose up` - Запуск docker-compose
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource;

@SpringBootApplication
@PropertySource("classpath:application.yml")
public class RentPlaceApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import kattsyn.dev.rentplace.dtos.CategoryDTO;
import kattsyn.dev.rentplace.entities.Category;
import kattsyn.dev.rentplace.entities.Image;
import kattsyn.dev.rentplace.services.CategoryService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

Expand Down Expand Up @@ -106,4 +109,25 @@ public ResponseEntity<Category> deleteCategory(
) {
return ResponseEntity.ok(categoryService.deleteById(id));
}

@Operation(
summary = "Загрузка фотографии для категории",
description = "Загрузка фотографии для категории"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Успешно", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Image.class))),
@ApiResponse(responseCode = "500", description = "Непредвиденная ошибка со стороны сервера", content = @Content)
})
@PostMapping(path = "/{id}/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Void> uploadImage(
@Parameter(
description = "Файл фотографии",
required = true,
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)
) @RequestParam("file") MultipartFile file,
@PathVariable
@Parameter(description = "id категории", example = "10") long id) {
categoryService.uploadImage(file, id);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import kattsyn.dev.rentplace.dtos.FacilityDTO;
import kattsyn.dev.rentplace.entities.Facility;
import kattsyn.dev.rentplace.entities.Image;
import kattsyn.dev.rentplace.services.FacilityService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

Expand All @@ -25,6 +28,27 @@ public class FacilityController {

private final FacilityService facilityService;

@Operation(
summary = "Загрузка фотографии для категории",
description = "Загрузка фотографии для категории"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Успешно", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Image.class))),
@ApiResponse(responseCode = "500", description = "Непредвиденная ошибка со стороны сервера", content = @Content)
})
@PostMapping(path = "/{id}/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Void> uploadImage(
@Parameter(
description = "Файл фотографии",
required = true,
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)
) @RequestParam("file") MultipartFile file,
@PathVariable
@Parameter(description = "id категории", example = "10") long id) {
facilityService.uploadImage(file, id);
return ResponseEntity.ok().build();
}

@Operation(summary = "Получение всех удобств", description = "Получение всех удобств")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Успешно", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Facility[].class))),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package kattsyn.dev.rentplace.controllers;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import kattsyn.dev.rentplace.entities.Image;
import kattsyn.dev.rentplace.enums.ImageType;
import kattsyn.dev.rentplace.services.ImageService;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("${api.path}/images")
@RequiredArgsConstructor
@Tag(name = "ImageController", description = "Для взаимодействия с фотографиями")
public class ImageController {

private final ImageService imageService;

@Operation(
summary = "Загрузка фотографии",
description = "Загрузка фотографии"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Успешно", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Image.class))),
@ApiResponse(responseCode = "500", description = "Непредвиденная ошибка со стороны сервера", content = @Content)
})
@PostMapping(path = "/", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Image> uploadImage(
@Parameter(
description = "Файл фотографии",
required = true,
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)
) @RequestParam("file") MultipartFile file,
@Parameter(
description = "Тип изображения",
required = true,
schema = @Schema(
implementation = ImageType.class)
) @RequestParam ImageType imageType) {
return ResponseEntity.ok(imageService.uploadImage(file, imageType));
}



@PostMapping(value = "/multiple/", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(
summary = "Массовая загрузка фотографий",
description = "Загружает несколько фотографий за один запрос. Максимальное количество файлов - 10. Максимальный размер каждого файла - 10MB.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Фотографии успешно загружены", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Image[].class))),
@ApiResponse(responseCode = "400", description = "Некорректный запрос (превышено кол-во файлов, неверный формат и т.д.)", content = @Content),
@ApiResponse(responseCode = "413", description = "Превышен максимальный размер запроса"),
@ApiResponse(responseCode = "500", description = "Внутренняя ошибка сервера")
})
public ResponseEntity<List<Image>> uploadMultipleImages(@Parameter(
description = "Массив файлов фотографий",
required = true,
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, schema = @Schema(type = "string", format = "binary"))
) @RequestPart("files") MultipartFile[] files) {
List<Image> savedImages = new ArrayList<>();

for (MultipartFile file : files) {
savedImages.add(imageService.uploadImage(file));
}

return ResponseEntity.ok(savedImages);
}

@Operation(
summary = "Получить фотографию",
description = "Получить фотографию по id"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Успешно", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = Resource.class))
}),
@ApiResponse(responseCode = "400", description = "Получен некорректный ID", content = @Content),
@ApiResponse(responseCode = "404", description = "Фотография не найдена", content = @Content),
@ApiResponse(responseCode = "500", description = "Непредвиденная ошибка со стороны сервера", content = @Content)
})
@GetMapping("/{id}")
public ResponseEntity<Resource> getImage(
@Parameter(description = "ID фотографии", required = true, example = "1")
@PathVariable long id) {
Image image = imageService.getImageById(id);
Resource file = imageService.getImageResource(image);

return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + image.getOriginalFileName() + "\"")
.contentType(MediaType.parseMediaType(image.getContentType()))
.body(file);
}

@DeleteMapping("/{id}")
@Operation(
summary = "Удалить фотографию",
description = "Удаляет фотографию и её метаданные",
responses = {
@ApiResponse(
responseCode = "200",
description = "Фото успешно удалено"),
@ApiResponse(
responseCode = "404",
description = "Фото не найдено")
}
)
public ResponseEntity<Void> deleteImage(
@Parameter(description = "ID фотографии", required = true, example = "1")
@PathVariable Long id) {

imageService.deleteImage(id);

return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import kattsyn.dev.rentplace.dtos.PropertyDTO;
import kattsyn.dev.rentplace.entities.Image;
import kattsyn.dev.rentplace.entities.Property;
import kattsyn.dev.rentplace.enums.ImageType;
import kattsyn.dev.rentplace.services.ImageService;
import kattsyn.dev.rentplace.services.PropertyService;
import kattsyn.dev.rentplace.utils.PathResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

Expand All @@ -24,6 +30,40 @@
public class PropertyController {

private final PropertyService propertyService;
private final ImageService imageService;

@PostMapping(value = "/{id}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(
summary = "Массовая загрузка фотографий",
description = "Загружает несколько фотографий за один запрос. Максимальное количество файлов - 10. Максимальный размер каждого файла - 10MB.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Фотографии успешно загружены", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Image[].class))),
@ApiResponse(responseCode = "400", description = "Некорректный запрос (превышено кол-во файлов, неверный формат и т.д.)", content = @Content),
@ApiResponse(responseCode = "413", description = "Превышен максимальный размер запроса"),
@ApiResponse(responseCode = "500", description = "Внутренняя ошибка сервера")
})
public ResponseEntity<List<Image>> uploadMultipleImages(
@PathVariable @Parameter(description = "id объявления", example = "10") long id,
@Parameter(
description = "Массив файлов фотографий",
required = true,
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, schema = @Schema(type = "string", format = "binary"))
) @RequestPart("files") MultipartFile[] files) {


List<Image> savedImages = propertyService.uploadImages(files, id);

return ResponseEntity.ok(savedImages);
}

@PostMapping("/{id}/image")
public Image uploadPropertyImage(
@PathVariable Long id,
@RequestParam MultipartFile file) {

String path = PathResolver.resolvePath(ImageType.PROPERTY, id);
return imageService.uploadImage(file, path);
}

@Operation(
summary = "Получение всех объявлений",
Expand Down Expand Up @@ -106,6 +146,4 @@ public ResponseEntity<Property> updateProperty(@PathVariable @Parameter(descript
public ResponseEntity<Property> deleteProperty(@PathVariable @Parameter(description = "id объявления", example = "10") long id) {
return new ResponseEntity<>(propertyService.deleteById(id), HttpStatus.NO_CONTENT);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ public class Category {
@Schema(description = "Название категории", example = "Кемпинг")
private String name;

@OneToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
@JoinColumn(name = "image_id")
private Image image;

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ public class Facility {
@Column(name = "name")
private String name;

@OneToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
@JoinColumn(name = "image_id")
private Image image;
}
33 changes: 33 additions & 0 deletions rentplace/src/main/java/kattsyn/dev/rentplace/entities/Image.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package kattsyn.dev.rentplace.entities;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@NoArgsConstructor
@Getter
@Setter
@Table(name = "images")
public class Image {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "image_id")
private long imageId;

@Column(name = "file_name", length = 400)
private String fileName;
@Column(name = "original_file_name")
private String originalFileName;
@Column(name = "content_type")
private String contentType;
@Column(name = "additional_path")
private String additionalPath;
@Column(name = "size")
private long size;
@Column(name = "is_preview_image")
private boolean isPreviewImage;

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import jakarta.persistence.*;
import lombok.*;

import java.util.List;

@Entity
@AllArgsConstructor
@NoArgsConstructor
Expand All @@ -21,7 +23,7 @@ public class Property {
@Schema(description = "Адрес имущества", example = "Россия, Воронеж, ул. Новосибирская, д.21")
private String address;

@Column(name = "description")
@Column(name = "description", length = 2000)
@Schema(description = "Описание имущества", example = "Уютная квартира с видом на водохранилище")
private String description;

Expand Down Expand Up @@ -53,4 +55,8 @@ public class Property {
@Column(name = "max_guests")
private int maxGuests;

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "property_id")
List<Image> images;

}
Loading