Merge pull request #6 from SlimusMinus/add_post-image

added image for post
This commit is contained in:
SlimusMinus
2026-06-01 01:57:55 +03:00
committed by GitHub
9 changed files with 152 additions and 40 deletions

View File

@@ -0,0 +1,16 @@
package com.krylov.refound.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("/uploads/**")
.addResourceLocations("file:uploads/"); // папка uploads/ в корне проекта
}
}

View File

@@ -1,24 +1,23 @@
package com.krylov.refound.controller; package com.krylov.refound.controller;
import com.krylov.refound.dto.PostFilter;
import com.krylov.refound.dto.PostRequest; import com.krylov.refound.dto.PostRequest;
import com.krylov.refound.dto.PostResponse; import com.krylov.refound.dto.PostResponse;
import com.krylov.refound.entity.Post; import com.krylov.refound.entity.Post;
import com.krylov.refound.repository.PostRepository; import com.krylov.refound.enums.PostType;
import com.krylov.refound.service.PostService; import com.krylov.refound.service.PostService;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -31,20 +30,38 @@ public class PostController {
private final PostService service; private final PostService service;
@PostMapping @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public PostRequest create(@RequestBody PostRequest request) { public PostRequest create(
@ModelAttribute PostRequest request,
@RequestPart(value = "image", required = false) MultipartFile image
) {
log.info(request.toString()); log.info(request.toString());
return service.create(request); return service.create(request, image);
} }
@GetMapping @GetMapping
public List<Post> getFeed() { public List<PostResponse> getFeed() {
return service.getFeed(); return service.getFeed();
} }
@PutMapping("/{id}") @PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public PostResponse update(@PathVariable Long id, @RequestBody PostRequest request) { public PostResponse update(
return service.update(id, request); @PathVariable Long id,
@RequestParam String type,
@RequestParam String title,
@RequestParam(required = false) String description,
@RequestParam(required = false) String category,
@RequestParam(required = false) String phone,
@RequestParam(value = "image", required = false) MultipartFile image
) {
PostRequest request = new PostRequest();
request.setType(PostType.from(type));
request.setTitle(title);
request.setDescription(description);
request.setCategory(category);
request.setPhone(phone);
return service.update(id, request, image);
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")

View File

@@ -19,4 +19,6 @@ public class PostRequest {
private String category; private String category;
private String phone; private String phone;
private String imageUrl;
} }

View File

@@ -1,8 +1,10 @@
package com.krylov.refound.dto; package com.krylov.refound.dto;
import com.krylov.refound.entity.Image;
import com.krylov.refound.enums.PostStatus; import com.krylov.refound.enums.PostStatus;
import com.krylov.refound.enums.PostType; import com.krylov.refound.enums.PostType;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import lombok.Data; import lombok.Data;
@Data @Data
@@ -18,5 +20,7 @@ public class PostResponse {
private PostStatus status; private PostStatus status;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private String userEmail; private String userEmail;
private String phone;
private List<String> images;
private boolean isFavorite; private boolean isFavorite;
} }

View File

@@ -1,5 +1,6 @@
package com.krylov.refound.entity; package com.krylov.refound.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
@@ -21,5 +22,6 @@ public class Image {
private String url; private String url;
@ManyToOne @ManyToOne
@JsonIgnore
private Post post; private Post post;
} }

View File

@@ -3,6 +3,7 @@ package com.krylov.refound.entity;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.krylov.refound.enums.PostStatus; import com.krylov.refound.enums.PostStatus;
import com.krylov.refound.enums.PostType; import com.krylov.refound.enums.PostType;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EnumType; import jakarta.persistence.EnumType;
@@ -11,8 +12,11 @@ import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -25,24 +29,19 @@ public class Post {
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Enumerated(EnumType.STRING)
private PostType type;
private String title; private String title;
@Column(length = 2000) @Column(length = 2000)
private String description; private String description;
private String category; private String category;
private String city; private String city;
private String district; private String district;
private Double latitude; private Double latitude;
private Double longitude; private Double longitude;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private String phone;
@Enumerated(EnumType.STRING)
private PostType type;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private PostStatus status; private PostStatus status;
@@ -51,5 +50,8 @@ public class Post {
@JsonIgnore @JsonIgnore
private User user; private User user;
private String phone; @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Image> images = new ArrayList<>();
} }

View File

@@ -5,6 +5,9 @@ import com.krylov.refound.dto.PostResponse;
import com.krylov.refound.entity.Post; import com.krylov.refound.entity.Post;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import com.krylov.refound.entity.Image;
import org.mapstruct.Named;
import java.util.List;
@Mapper(componentModel = "spring") @Mapper(componentModel = "spring")
public interface PostMapper { public interface PostMapper {
@@ -12,5 +15,14 @@ public interface PostMapper {
Post toEntity(PostRequest request); Post toEntity(PostRequest request);
@Mapping(source = "user.login", target = "userEmail") @Mapping(source = "user.login", target = "userEmail")
@Mapping(source = "images", target = "images", qualifiedByName = "imagesToUrls")
PostResponse toResponse(Post post); PostResponse toResponse(Post post);
@Named("imagesToUrls")
default List<String> imagesToUrls(List<Image> images) {
if (images == null) return List.of();
return images.stream()
.map(Image::getUrl)
.toList();
}
} }

View File

@@ -1,58 +1,75 @@
package com.krylov.refound.service; package com.krylov.refound.service;
import com.krylov.refound.dto.PostFilter;
import com.krylov.refound.dto.PostRequest; import com.krylov.refound.dto.PostRequest;
import com.krylov.refound.dto.PostResponse; import com.krylov.refound.dto.PostResponse;
import com.krylov.refound.entity.Image; import com.krylov.refound.entity.Image;
import com.krylov.refound.entity.Post; import com.krylov.refound.entity.Post;
import com.krylov.refound.entity.User;
import com.krylov.refound.enums.ErrorCode; import com.krylov.refound.enums.ErrorCode;
import com.krylov.refound.enums.PostStatus; import com.krylov.refound.enums.PostStatus;
import com.krylov.refound.exception.ApiException; import com.krylov.refound.exception.ApiException;
import com.krylov.refound.mapper.PostMapper; import com.krylov.refound.mapper.PostMapper;
import com.krylov.refound.repository.FavoriteRepository;
import com.krylov.refound.repository.ImageRepository; import com.krylov.refound.repository.ImageRepository;
import com.krylov.refound.repository.PostRepository; import com.krylov.refound.repository.PostRepository;
import com.krylov.refound.util.PostSpecification; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.UUID;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class PostService { public class PostService {
private final PostRepository repository; private final PostRepository repository;
private final ImageRepository imageRepository;
private final FileStorageService storageService;
private final PostMapper mapper; private final PostMapper mapper;
private final UserService userService; private final ImageRepository imageRepository;
private final FavoriteRepository favoriteRepository;
@CacheEvict(value = "posts", allEntries = true) @CacheEvict(value = "posts", allEntries = true)
public PostRequest create(PostRequest request) { public PostRequest create(PostRequest request, MultipartFile image) {
Post post = mapper.toEntity(request); Post post = mapper.toEntity(request);
post.setCreatedAt(LocalDateTime.now()); post.setCreatedAt(LocalDateTime.now());
post.setStatus(PostStatus.ACTIVE); post.setStatus(PostStatus.ACTIVE);
repository.save(post); // сначала сохраняем post, чтобы получить id
repository.save(post); if (image != null && !image.isEmpty()) {
String filename = UUID.randomUUID() + "_" + image.getOriginalFilename();
Path uploadDir = Paths.get("uploads");
try {
Files.createDirectories(uploadDir);
Files.copy(image.getInputStream(), uploadDir.resolve(filename));
} catch (IOException e) {
throw new RuntimeException("Не удалось сохранить файл: " + filename, e);
}
Image img = new Image();
img.setUrl("/uploads/" + filename);
img.setPost(post);
imageRepository.save(img);
}
return request; return request;
} }
public List<Post> getFeed() { public List<PostResponse> getFeed() {
List<Post> postList = repository.findAll(); return repository.findAll().stream()
return postList; .map(post -> {
PostResponse response = mapper.toResponse(post);
log.info("Post {}: images = {}, phone = {}", post.getId(), response.getImages()); // ← сюда
return response;
}).toList();
} }
@Cacheable(value = "post", key = "#id") @Cacheable(value = "post", key = "#id")
@@ -64,7 +81,7 @@ public class PostService {
} }
@CacheEvict(value = {"posts", "post"}, allEntries = true) @CacheEvict(value = {"posts", "post"}, allEntries = true)
public PostResponse update(Long id, PostRequest request) { public PostResponse update(Long id, PostRequest request, MultipartFile image) {
Post post = repository.findById(id) Post post = repository.findById(id)
.orElseThrow(() -> new ApiException( .orElseThrow(() -> new ApiException(
@@ -79,8 +96,35 @@ public class PostService {
post.setPhone(request.getPhone()); post.setPhone(request.getPhone());
post.setType(request.getType()); post.setType(request.getType());
Post updated = repository.save(post); if (image != null && !image.isEmpty()) {
// удаляем старые картинки с диска
post.getImages().forEach(img -> {
try {
Files.deleteIfExists(Paths.get("." + img.getUrl()));
} catch (IOException e) {
log.warn("Не удалось удалить файл: {}", img.getUrl());
}
});
// очищаем список (orphanRemoval = true удалит из БД)
post.getImages().clear();
// сохраняем новую картинку
String filename = UUID.randomUUID() + "_" + image.getOriginalFilename();
Path uploadDir = Paths.get("uploads");
try {
Files.createDirectories(uploadDir);
Files.copy(image.getInputStream(), uploadDir.resolve(filename));
} catch (IOException e) {
throw new RuntimeException("Не удалось сохранить файл: " + filename, e);
}
Image img = new Image();
img.setUrl("/uploads/" + filename);
img.setPost(post);
post.getImages().add(img);
}
Post updated = repository.save(post);
return mapper.toResponse(updated); return mapper.toResponse(updated);
} }

View File

@@ -0,0 +1,13 @@
package com.krylov.refound.util;
import com.krylov.refound.enums.PostType;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
@Component
public class StringToPostTypeConverter implements Converter<String, PostType> {
@Override
public PostType convert(String value) {
return PostType.from(value);
}
}