fixed controllers
This commit is contained in:
77
src/main/java/com/krylov/refound/service/AuthService.java
Normal file
77
src/main/java/com/krylov/refound/service/AuthService.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package com.krylov.refound.service;
|
||||
|
||||
import com.krylov.refound.dto.AuthRequest;
|
||||
import com.krylov.refound.dto.CheckLoginDto;
|
||||
import com.krylov.refound.dto.RegisterRequest;
|
||||
import com.krylov.refound.dto.UserDto;
|
||||
import com.krylov.refound.dto.UserRegisterDto;
|
||||
import com.krylov.refound.entity.User;
|
||||
import com.krylov.refound.exception.InvalidCredentialsException;
|
||||
import com.krylov.refound.exception.LoginAlreadyExistsException;
|
||||
import com.krylov.refound.repository.UserRepository;
|
||||
import com.krylov.refound.security.JwtService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuthService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtService jwtService;
|
||||
|
||||
public UserRegisterDto registerUser(RegisterRequest request){
|
||||
if (userRepository.findByLogin(request.getLogin()).isPresent()) {
|
||||
log.warn("registration failed: login {} already taken", request.getLogin());
|
||||
throw new LoginAlreadyExistsException("Логин уже занят");
|
||||
}
|
||||
User user = new User();
|
||||
user.setName(request.getName());
|
||||
user.setLogin(request.getLogin());
|
||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
|
||||
userRepository.save(user);
|
||||
|
||||
String token = jwtService.generateToken(user.getLogin());
|
||||
|
||||
UserDto userDto = getUserDto(user);
|
||||
|
||||
return new UserRegisterDto(userDto, token);
|
||||
}
|
||||
|
||||
public UserRegisterDto loginUser(AuthRequest request){
|
||||
User user = userRepository.findByLogin(request.getLogin()).orElse(null);
|
||||
|
||||
if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) {
|
||||
log.warn("login for user {} or password is incorrect", request.getLogin());
|
||||
throw new InvalidCredentialsException("Неверный логин или пароль");
|
||||
}
|
||||
|
||||
String token = jwtService.generateToken(user.getLogin());
|
||||
UserDto userDto = getUserDto(user);
|
||||
|
||||
return new UserRegisterDto(userDto, token);
|
||||
}
|
||||
|
||||
public boolean existsByLogin(CheckLoginDto request){
|
||||
return userRepository.existsByLogin(request.getLogin());
|
||||
}
|
||||
|
||||
private static UserDto getUserDto(User user) {
|
||||
return new UserDto(
|
||||
user.getId(),
|
||||
user.getName(),
|
||||
user.getLastName(),
|
||||
user.getLogin(),
|
||||
user.getEmail(),
|
||||
user.getPhone(),
|
||||
user.getAvatarUrl(),
|
||||
user.getRole()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.krylov.refound.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Service
|
||||
public class AvatarService {
|
||||
private final Path uploadDir = Paths.get("uploads/avatars");
|
||||
|
||||
public String save(MultipartFile file) {
|
||||
try {
|
||||
Files.createDirectories(uploadDir);
|
||||
String filename = UUID.randomUUID() + "_" + file.getOriginalFilename();
|
||||
Path target = uploadDir.resolve(filename);
|
||||
file.transferTo(target);
|
||||
return "/uploads/avatars/" + filename; // или полный URL, если есть статик-сервер/CDN
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to save avatar", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,17 +11,22 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ChatService {
|
||||
|
||||
private final ChatRepository chatRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final UserRepository userRepository; // твой существующий репозиторий юзеров
|
||||
private final UserRepository userRepository;
|
||||
|
||||
// Создать чат с пользователем (или вернуть существующий)
|
||||
@Transactional
|
||||
@@ -46,28 +51,28 @@ public class ChatService {
|
||||
}
|
||||
|
||||
// Все чаты пользователя
|
||||
@Transactional(readOnly = true)
|
||||
public List<ChatDto> getUserChats(Long userId) {
|
||||
return chatRepository.findAllByUserId(userId).stream()
|
||||
.map(c -> toDto(c, userId))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// История сообщений в чате
|
||||
public List<MessageDto> getMessages(Long chatId) {
|
||||
return messageRepository.findByChatIdOrderByCreatedAtAsc(chatId).stream()
|
||||
.map(this::toMessageDto)
|
||||
.collect(Collectors.toList());
|
||||
// История сообщений в чате, с пагинацией и проверкой доступа
|
||||
@Transactional(readOnly = true)
|
||||
public Page<MessageDto> getMessages(Long chatId, Long userId, Pageable pageable) {
|
||||
Chat chat = getChatOrThrow(chatId);
|
||||
checkParticipant(chat, userId);
|
||||
|
||||
return messageRepository.findByChatIdOrderByCreatedAtDesc(chatId, pageable)
|
||||
.map(this::toMessageDto);
|
||||
}
|
||||
|
||||
// Отправка сообщения (вызывается из WebSocket контроллера)
|
||||
@Transactional
|
||||
public MessageDto sendMessage(Long senderId, Long chatId, String content) {
|
||||
Chat chat = chatRepository.findById(chatId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Чат не найден"));
|
||||
|
||||
if (!chat.getUserOneId().equals(senderId) && !chat.getUserTwoId().equals(senderId)) {
|
||||
throw new SecurityException("Вы не участник этого чата");
|
||||
}
|
||||
Chat chat = getChatOrThrow(chatId);
|
||||
checkParticipant(chat, senderId);
|
||||
|
||||
Message message = new Message();
|
||||
message.setChatId(chatId);
|
||||
@@ -95,6 +100,33 @@ public class ChatService {
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteChat(Long chatId, Long userId) {
|
||||
Chat chat = getChatOrThrow(chatId);
|
||||
checkParticipant(chat, userId);
|
||||
chatRepository.deleteById(chatId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markAsRead(Long chatId, Long userId) {
|
||||
Chat chat = getChatOrThrow(chatId);
|
||||
checkParticipant(chat, userId);
|
||||
messageRepository.markAsReadByChatIdAndRecipient(chatId, userId);
|
||||
}
|
||||
|
||||
// ---------- вспомогательные методы ----------
|
||||
|
||||
private Chat getChatOrThrow(Long chatId) {
|
||||
return chatRepository.findById(chatId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Чат не найден"));
|
||||
}
|
||||
|
||||
private void checkParticipant(Chat chat, Long userId) {
|
||||
if (!chat.getUserOneId().equals(userId) && !chat.getUserTwoId().equals(userId)) {
|
||||
throw new SecurityException("Вы не участник этого чата");
|
||||
}
|
||||
}
|
||||
|
||||
private ChatDto toDto(Chat chat, Long currentUserId) {
|
||||
Long otherUserId = chat.getUserOneId().equals(currentUserId)
|
||||
? chat.getUserTwoId() : chat.getUserOneId();
|
||||
@@ -104,18 +136,17 @@ public class ChatService {
|
||||
dto.setOtherUserId(otherUserId);
|
||||
dto.setPostId(chat.getPostId());
|
||||
|
||||
// Подтягиваем имя/аватар собеседника из твоего UserRepository
|
||||
userRepository.findById(otherUserId).ifPresent(user -> {
|
||||
dto.setOtherUserName(user.getName() + " " + user.getLastName());
|
||||
dto.setOtherUserAvatar(user.getAvatarUrl());
|
||||
});
|
||||
|
||||
List<Message> messages = messageRepository.findByChatIdOrderByCreatedAtAsc(chat.getId());
|
||||
if (!messages.isEmpty()) {
|
||||
Message last = messages.getLast();
|
||||
dto.setLastMessage(last.getContent());
|
||||
dto.setLastMessageAt(last.getCreatedAt());
|
||||
}
|
||||
// без N+1: берём только последнее сообщение, а не всю историю
|
||||
messageRepository.findFirstByChatIdOrderByCreatedAtDesc(chat.getId())
|
||||
.ifPresent(last -> {
|
||||
dto.setLastMessage(last.getContent());
|
||||
dto.setLastMessageAt(last.getCreatedAt());
|
||||
});
|
||||
|
||||
dto.setUnreadCount(
|
||||
messageRepository.countByChatIdAndIsReadFalseAndSenderIdNot(chat.getId(), currentUserId)
|
||||
@@ -134,13 +165,4 @@ public class ChatService {
|
||||
dto.setCreatedAt(m.getCreatedAt());
|
||||
return dto;
|
||||
}
|
||||
|
||||
public void deleteChat(Long chatId) {
|
||||
chatRepository.deleteById(chatId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markAsRead(Long chatId, Long userId) {
|
||||
messageRepository.markAsReadByChatIdAndRecipient(chatId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package com.krylov.refound.service;
|
||||
|
||||
import com.krylov.refound.dto.PostResponse;
|
||||
import com.krylov.refound.entity.Favorite;
|
||||
import com.krylov.refound.entity.Post;
|
||||
import com.krylov.refound.entity.User;
|
||||
import com.krylov.refound.enums.ErrorCode;
|
||||
import com.krylov.refound.exception.ApiException;
|
||||
import com.krylov.refound.mapper.PostMapper;
|
||||
import com.krylov.refound.repository.FavoriteRepository;
|
||||
import com.krylov.refound.repository.PostRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -18,44 +21,19 @@ public class FavoriteService {
|
||||
private final FavoriteRepository repository;
|
||||
private final PostRepository postRepository;
|
||||
private final UserService userService;
|
||||
private final PostMapper mapper;
|
||||
|
||||
public void addToFavorites(Long postId) {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PostResponse> getFavorites() {
|
||||
User user = userService.getCurrentUser();
|
||||
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "Post not found", HttpStatus.NOT_FOUND));
|
||||
|
||||
if (repository.existsByUserAndPost(user, post)) {
|
||||
return; // уже есть
|
||||
}
|
||||
|
||||
Favorite favorite = new Favorite();
|
||||
favorite.setUser(user);
|
||||
favorite.setPost(post);
|
||||
|
||||
repository.save(favorite);
|
||||
}
|
||||
|
||||
public void removeFromFavorites(Long postId) {
|
||||
|
||||
User user = userService.getCurrentUser();
|
||||
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "Post not found", HttpStatus.NOT_FOUND));
|
||||
|
||||
repository.deleteByUserAndPost(user, post);
|
||||
}
|
||||
|
||||
public List<Post> getFavorites() {
|
||||
User user = userService.getCurrentUser();
|
||||
|
||||
return repository.findByUser(user)
|
||||
.stream()
|
||||
.map(Favorite::getPost)
|
||||
.map(mapper::toResponse)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void toggle(Long postId) {
|
||||
|
||||
User user = userService.getCurrentUser();
|
||||
|
||||
@@ -1,140 +1,203 @@
|
||||
package com.krylov.refound.service;
|
||||
|
||||
import com.krylov.refound.config.MinioProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import software.amazon.awssdk.core.ResponseBytes;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.*;
|
||||
|
||||
import com.krylov.refound.enums.ErrorCode;
|
||||
import com.krylov.refound.exception.ApiException;
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
import software.amazon.awssdk.core.ResponseBytes;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
|
||||
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
|
||||
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class FileStorageService {
|
||||
|
||||
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
private static final Set<String> ALLOWED_CONTENT_TYPES = Set.of(
|
||||
"image/jpeg", "image/png", "image/webp"
|
||||
);
|
||||
|
||||
private final S3Client s3Client;
|
||||
private final MinioProperties properties;
|
||||
|
||||
/**
|
||||
* DTO для передачи данных скачанного файла.
|
||||
* @param originalFilename Оригинальное имя файла.
|
||||
* @param contentType MIME-тип файла.
|
||||
* @param content Содержимое файла в виде массива байт.
|
||||
*/
|
||||
public record DownloadedFile(
|
||||
String originalFilename,
|
||||
String contentType,
|
||||
byte[] content
|
||||
private record DownloadedFile(
|
||||
String originalFilename,
|
||||
String contentType,
|
||||
byte[] content
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Загружает файл в MinIO, сохраняя оригинальное имя в метаданных,
|
||||
* и возвращает его уникальное имя (включая расширение).
|
||||
*
|
||||
* @param file Загружаемый файл.
|
||||
* @return Сгенерированное уникальное имя файла.
|
||||
* Загружает файл в MinIO в корень бакета.
|
||||
*/
|
||||
public String uploadFile(MultipartFile file) {
|
||||
return uploadFile(file, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает файл в MinIO в указанную "папку" (префикс ключа),
|
||||
* например "avatars" — сохранится как avatars/<uuid>.ext
|
||||
*/
|
||||
public String uploadFile(MultipartFile file, String folder) {
|
||||
validateFile(file);
|
||||
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String extension = extractExtension(originalFilename);
|
||||
String prefix = (folder != null && !folder.isBlank())
|
||||
? folder.replaceAll("/+$", "") + "/"
|
||||
: "";
|
||||
String objectName = prefix + UUID.randomUUID() + extension;
|
||||
|
||||
log.info("Загрузка файла '{}'. Новое имя: {}", originalFilename, objectName);
|
||||
|
||||
try {
|
||||
if (file.isEmpty()) {
|
||||
log.error("Попытка загрузить пустой файл.");
|
||||
throw new RuntimeException("Файл не может быть пустым.");
|
||||
}
|
||||
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String extension = "";
|
||||
if (originalFilename != null && originalFilename.contains(".")) {
|
||||
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
|
||||
}
|
||||
|
||||
String objectName = UUID.randomUUID().toString() + extension;
|
||||
log.info("Загрузка файла '{}'. Новое имя: {}", originalFilename, objectName);
|
||||
|
||||
Map<String, String> metadata = new HashMap<>();
|
||||
metadata.put("original-filename", URLEncoder.encode(originalFilename, StandardCharsets.UTF_8));
|
||||
|
||||
PutObjectRequest request = PutObjectRequest.builder()
|
||||
.bucket(properties.bucket())
|
||||
.key(objectName)
|
||||
.contentType(file.getContentType())
|
||||
.metadata(metadata)
|
||||
.build();
|
||||
.bucket(properties.bucket())
|
||||
.key(objectName)
|
||||
.contentType(file.getContentType())
|
||||
.metadata(metadata)
|
||||
.build();
|
||||
|
||||
s3Client.putObject(request, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
|
||||
log.info("Файл '{}' успешно загружен.", objectName);
|
||||
return objectName;
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("Ошибка при чтении файла для загрузки: {}", e.getMessage());
|
||||
throw new RuntimeException("Ошибка при обработке файла.", e);
|
||||
log.error("Ошибка при чтении файла для загрузки: {}", e.getMessage(), e);
|
||||
throw new ApiException(ErrorCode.INTERNAL_ERROR, "Ошибка при обработке файла.", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
} catch (Exception e) {
|
||||
log.error("Не удалось загрузить файл: {}", e.getMessage());
|
||||
throw new RuntimeException("Ошибка при загрузке файла.", e);
|
||||
log.error("Не удалось загрузить файл: {}", e.getMessage(), e);
|
||||
throw new ApiException(ErrorCode.INTERNAL_ERROR, "Ошибка при загрузке файла.", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Скачивает файл из MinIO.
|
||||
*
|
||||
* @param objectName Уникальное имя файла в хранилище.
|
||||
* @return DTO с содержимым файла и его метаданными.
|
||||
* Скачивает файл из MinIO и собирает готовый HTTP-ответ с нужными заголовками.
|
||||
* Вся логика формирования Content-Disposition/Content-Type инкапсулирована здесь,
|
||||
* контроллеру остаётся только вернуть результат.
|
||||
*/
|
||||
public DownloadedFile downloadFile(String objectName) {
|
||||
try {
|
||||
// 1. Получаем метаданные объекта, чтобы извлечь оригинальное имя
|
||||
HeadObjectRequest headRequest = HeadObjectRequest.builder()
|
||||
.bucket(properties.bucket())
|
||||
.key(objectName)
|
||||
.build();
|
||||
HeadObjectResponse headResponse = s3Client.headObject(headRequest);
|
||||
// Декодируем имя файла из метаданных
|
||||
String originalFilename = URLDecoder.decode(headResponse.metadata().get("original-filename"), StandardCharsets.UTF_8);
|
||||
String contentType = headResponse.contentType();
|
||||
public ResponseEntity<byte[]> downloadFile(String objectName) {
|
||||
DownloadedFile file = fetchFile(objectName);
|
||||
|
||||
// 2. Получаем сам объект (его содержимое)
|
||||
GetObjectRequest getRequest = GetObjectRequest.builder()
|
||||
.bucket(properties.bucket())
|
||||
.key(objectName)
|
||||
.build();
|
||||
ResponseBytes<GetObjectResponse> responseBytes = s3Client.getObjectAsBytes(getRequest);
|
||||
String encodedFilename = UriUtils.encode(file.originalFilename(), StandardCharsets.UTF_8);
|
||||
String disposition = file.contentType().startsWith("image/") ? "inline" : "attachment";
|
||||
|
||||
log.info("Файл '{}' (оригинальное имя: '{}') успешно скачан.", objectName, originalFilename);
|
||||
return new DownloadedFile(originalFilename, contentType, responseBytes.asByteArray());
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(HttpHeaders.CONTENT_DISPOSITION, disposition + "; filename*=UTF-8''" + encodedFilename);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Ошибка при скачивании файла '{}': {}", objectName, e.getMessage());
|
||||
throw new RuntimeException("Ошибка при скачивании файла.", e);
|
||||
}
|
||||
return ResponseEntity.ok()
|
||||
.headers(headers)
|
||||
.contentType(MediaType.parseMediaType(file.contentType()))
|
||||
.body(file.content());
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет файл из MinIO по его имени.
|
||||
*
|
||||
* @param objectName Имя файла для удаления.
|
||||
*/
|
||||
public void deleteFile(String objectName) {
|
||||
try {
|
||||
log.info("Запрос на удаление файла: {}", objectName);
|
||||
DeleteObjectRequest request = DeleteObjectRequest.builder()
|
||||
.bucket(properties.bucket())
|
||||
.key(objectName)
|
||||
.build();
|
||||
.bucket(properties.bucket())
|
||||
.key(objectName)
|
||||
.build();
|
||||
|
||||
s3Client.deleteObject(request);
|
||||
log.info("Файл '{}' успешно удален.", objectName);
|
||||
} catch (Exception e) {
|
||||
log.error("Ошибка при удалении файла '{}': {}", objectName, e.getMessage());
|
||||
throw new RuntimeException("Ошибка при удалении файла.", e);
|
||||
log.error("Ошибка при удалении файла '{}': {}", objectName, e.getMessage(), e);
|
||||
throw new ApiException(ErrorCode.INTERNAL_ERROR, "Ошибка при удалении файла.", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- вспомогательные методы ----------
|
||||
|
||||
private DownloadedFile fetchFile(String objectName) {
|
||||
try {
|
||||
HeadObjectRequest headRequest = HeadObjectRequest.builder()
|
||||
.bucket(properties.bucket())
|
||||
.key(objectName)
|
||||
.build();
|
||||
HeadObjectResponse headResponse = s3Client.headObject(headRequest);
|
||||
|
||||
String originalFilename = URLDecoder.decode(
|
||||
headResponse.metadata().get("original-filename"), StandardCharsets.UTF_8);
|
||||
|
||||
String contentType = headResponse.contentType() != null
|
||||
? headResponse.contentType()
|
||||
: MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
|
||||
GetObjectRequest getRequest = GetObjectRequest.builder()
|
||||
.bucket(properties.bucket())
|
||||
.key(objectName)
|
||||
.build();
|
||||
ResponseBytes<GetObjectResponse> responseBytes = s3Client.getObjectAsBytes(getRequest);
|
||||
|
||||
log.info("Файл '{}' (оригинальное имя: '{}') успешно скачан.", objectName, originalFilename);
|
||||
return new DownloadedFile(originalFilename, contentType, responseBytes.asByteArray());
|
||||
|
||||
} catch (NoSuchKeyException e) {
|
||||
log.warn("Файл не найден: {}", objectName);
|
||||
throw new ApiException(ErrorCode.NOT_FOUND, "Файл не найден.", HttpStatus.NOT_FOUND);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("Ошибка при скачивании файла '{}': {}", objectName, e.getMessage(), e);
|
||||
throw new ApiException(ErrorCode.INTERNAL_ERROR, "Ошибка при скачивании файла.", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFile(MultipartFile file) {
|
||||
if (file.isEmpty()) {
|
||||
log.warn("Попытка загрузить пустой файл.");
|
||||
throw new ApiException(ErrorCode.VALIDATION_ERROR, "Файл не может быть пустым.", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (file.getSize() > MAX_FILE_SIZE) {
|
||||
log.warn("Файл превышает допустимый размер: {} байт.", file.getSize());
|
||||
throw new ApiException(ErrorCode.VALIDATION_ERROR, "Файл слишком большой (максимум 10 МБ).", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
String contentType = file.getContentType();
|
||||
if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) {
|
||||
log.warn("Недопустимый тип файла: {}", contentType);
|
||||
throw new ApiException(ErrorCode.VALIDATION_ERROR, "Недопустимый тип файла.", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private String extractExtension(String originalFilename) {
|
||||
if (originalFilename != null && originalFilename.contains(".")) {
|
||||
return originalFilename.substring(originalFilename.lastIndexOf("."));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.krylov.refound.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.krylov.refound.ai.facade.ContentModerationFacade;
|
||||
import com.krylov.refound.dto.PostRequest;
|
||||
import com.krylov.refound.dto.PostResponse;
|
||||
@@ -9,6 +11,7 @@ import com.krylov.refound.entity.User;
|
||||
import com.krylov.refound.enums.ErrorCode;
|
||||
import com.krylov.refound.enums.PostCategory;
|
||||
import com.krylov.refound.enums.PostStatus;
|
||||
import com.krylov.refound.enums.PostType;
|
||||
import com.krylov.refound.exception.ApiException;
|
||||
import com.krylov.refound.mapper.PostMapper;
|
||||
import com.krylov.refound.repository.FavoriteRepository;
|
||||
@@ -20,6 +23,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -52,6 +56,7 @@ public class PostService {
|
||||
private final FileStorageService fileStorageService;
|
||||
private final ContentModerationFacade contentModerationFacade;
|
||||
private final StatisticsService statisticsService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public PostRequest create(PostRequest request, List<MultipartFile> images) {
|
||||
|
||||
@@ -153,6 +158,35 @@ public class PostService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
public PostResponse update(
|
||||
Long id,
|
||||
String type,
|
||||
String title,
|
||||
String address,
|
||||
Double latitude,
|
||||
Double longitude,
|
||||
String description,
|
||||
String category,
|
||||
String phone,
|
||||
List<MultipartFile> images,
|
||||
String existingImagesJson) {
|
||||
|
||||
PostRequest request = new PostRequest();
|
||||
request.setType(PostType.from(type));
|
||||
request.setTitle(title);
|
||||
request.setDescription(description);
|
||||
request.setCategory(category);
|
||||
request.setPhone(phone);
|
||||
request.setLatitude(latitude);
|
||||
request.setLongitude(longitude);
|
||||
request.setAddress(address);
|
||||
|
||||
List<String> existingUrls = parseExistingImages(existingImagesJson);
|
||||
|
||||
return update(id, request, images, existingUrls);
|
||||
}
|
||||
|
||||
// старый метод остаётся как есть — рефакторить будем позже
|
||||
public PostResponse update(Long id, PostRequest request, List<MultipartFile> images,
|
||||
List<String> existingUrls) {
|
||||
Post post = repository.findById(id)
|
||||
@@ -217,6 +251,21 @@ public class PostService {
|
||||
return mapper.toResponse(updated);
|
||||
}
|
||||
|
||||
private List<String> parseExistingImages(String existingImagesJson) {
|
||||
if (existingImagesJson == null) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
return Arrays.asList(objectMapper.readValue(existingImagesJson, String[].class));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new ApiException(
|
||||
ErrorCode.VALIDATION_ERROR,
|
||||
"Некорректный формат existingImages",
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@CacheEvict(value = {"posts", "post"}, allEntries = true)
|
||||
public void delete(Long id) {
|
||||
Post post = repository.findById(id)
|
||||
|
||||
@@ -20,7 +20,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
@RequiredArgsConstructor
|
||||
public class UserService {
|
||||
private final UserRepository repository;
|
||||
private final AvatarService avatarService;
|
||||
private final FileStorageService fileStorageService;
|
||||
|
||||
public User getCurrentUser() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
@@ -55,8 +55,19 @@ public class UserService {
|
||||
|
||||
MultipartFile avatar = dto.getAvatar();
|
||||
if (avatar != null && !avatar.isEmpty()) {
|
||||
String url = avatarService.save(avatar); // сохранение на диск/в S3/Yandex Object Storage
|
||||
user.setAvatarUrl(url);
|
||||
String oldAvatarKey = user.getAvatarUrl();
|
||||
|
||||
String newAvatarKey = fileStorageService.uploadFile(avatar, "avatars");
|
||||
user.setAvatarUrl(newAvatarKey);
|
||||
|
||||
// подчищаем старый файл, если он был и это реально ключ MinIO
|
||||
if (oldAvatarKey != null && !oldAvatarKey.isBlank()) {
|
||||
try {
|
||||
fileStorageService.deleteFile(oldAvatarKey);
|
||||
} catch (Exception e) {
|
||||
log.warn("Не удалось удалить старый аватар из хранилища: {}", oldAvatarKey, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repository.save(user);
|
||||
|
||||
Reference in New Issue
Block a user