added posts on feed and posts on map
This commit is contained in:
@@ -3,6 +3,7 @@ package com.krylov.refound.service;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.krylov.refound.dto.AddressInfo;
|
||||
import com.krylov.refound.dto.CachedPostPage;
|
||||
import com.krylov.refound.dto.PostRequest;
|
||||
import com.krylov.refound.dto.PostResponse;
|
||||
import com.krylov.refound.entity.Image;
|
||||
@@ -16,7 +17,12 @@ import com.krylov.refound.exception.ApiException;
|
||||
import com.krylov.refound.mapper.PostMapper;
|
||||
import com.krylov.refound.repository.ImageRepository;
|
||||
import com.krylov.refound.repository.PostRepository;
|
||||
import com.krylov.refound.service.redis.MapCacheService;
|
||||
import com.krylov.refound.service.redis.PostCacheKeyGenerator;
|
||||
import com.krylov.refound.service.redis.PostCacheService;
|
||||
import com.krylov.refound.util.PostSpecification;
|
||||
import com.krylov.refound.util.PostVisibility;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
@@ -25,6 +31,7 @@ import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -38,8 +45,8 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
@Slf4j
|
||||
public class PostService {
|
||||
// Статусы, которые показываются в публичной ленте. MODERATION и REJECTED видны
|
||||
// только владельцу через getPostsByUserId, но не в общем фиде.
|
||||
private static final Set<PostStatus> FEED_VISIBLE_STATUSES = EnumSet.of(PostStatus.ACTIVE, PostStatus.CLOSED);
|
||||
// только владельцу (getPostsByUserId, getPostOnMapById), но не в общем фиде.
|
||||
private static final Set<PostStatus> FEED_VISIBLE_STATUSES = PostVisibility.FEED_VISIBLE_STATUSES;
|
||||
|
||||
private final PostRepository postRepository;
|
||||
private final PostMapper mapper;
|
||||
@@ -49,6 +56,9 @@ public class PostService {
|
||||
private final FileStorageService fileStorageService;
|
||||
private final StatisticsService statisticsService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final PostCacheService postCacheService;
|
||||
private final PostCacheKeyGenerator postCacheKeyGenerator;
|
||||
private final MapCacheService mapCacheService;
|
||||
|
||||
@Transactional
|
||||
public PostRequest create(PostRequest request, List<MultipartFile> images) {
|
||||
@@ -75,27 +85,58 @@ public class PostService {
|
||||
log.info("К посту id={} прикреплено {} изображений", post.getId(), uploaded);
|
||||
|
||||
statisticsService.incrementCreated();
|
||||
postCacheService.invalidatePosts();
|
||||
mapCacheService.clearMapMarkersCache();
|
||||
return request;
|
||||
}
|
||||
|
||||
public Page<PostResponse> getFeed(String search, Pageable pageable) {
|
||||
|
||||
// Ключ содержит версию ленты, она читается до запроса в БД
|
||||
String cacheKey = postCacheKeyGenerator.feedKey(search, pageable);
|
||||
|
||||
CachedPostPage cached = postCacheService.getFeed(cacheKey);
|
||||
|
||||
if (cached != null) {
|
||||
log.debug("Feed cache HIT: {}", cacheKey);
|
||||
return new PageImpl<>(cached.getContent(), pageable, cached.getTotalElements());
|
||||
}
|
||||
|
||||
log.debug("Feed cache MISS: {}", cacheKey);
|
||||
|
||||
Specification<Post> spec = PostSpecification.hasSearch(search)
|
||||
.and((root, query, cb) ->
|
||||
root.get("status").in(FEED_VISIBLE_STATUSES));
|
||||
root.get("status").in(FEED_VISIBLE_STATUSES)
|
||||
);
|
||||
|
||||
Page<Post> postsPage = postRepository.findAll(spec, pageable);
|
||||
|
||||
return postsPage.map(post -> toResponse(post, true));
|
||||
Page<PostResponse> responsePage = postsPage.map(post -> toResponse(post, true));
|
||||
|
||||
CachedPostPage cachedPage = new CachedPostPage(
|
||||
responsePage.getContent(),
|
||||
responsePage.getNumber(),
|
||||
responsePage.getSize(),
|
||||
responsePage.getTotalElements(),
|
||||
responsePage.getTotalPages()
|
||||
);
|
||||
|
||||
postCacheService.saveFeed(cacheKey, cachedPage, Duration.ofMinutes(2));
|
||||
return responsePage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает все посты указанного пользователя (это НЕ поиск поста по его собственному id,
|
||||
* Возвращает посты указанного пользователя (это НЕ поиск поста по его собственному id,
|
||||
* несмотря на то, как метод назывался раньше — getById). Если у вас уже есть контроллер,
|
||||
* вызывающий старое имя getById, переименуйте вызов на getPostsByUserId.
|
||||
* Владелец видит все свои посты (включая MODERATION и REJECTED), остальные — только
|
||||
* опубликованные (ACTIVE и CLOSED).
|
||||
*/
|
||||
public List<PostResponse> getPostsByUserId(Long userId) {
|
||||
boolean owner = isCurrentUser(userId);
|
||||
|
||||
List<Post> posts = postRepository.findByUserId(userId).stream()
|
||||
.filter(post -> post.getStatus() == PostStatus.ACTIVE || post.getStatus() == PostStatus.CLOSED)
|
||||
.filter(post -> owner || FEED_VISIBLE_STATUSES.contains(post.getStatus()))
|
||||
.toList();
|
||||
|
||||
if (posts.isEmpty()) {
|
||||
@@ -136,8 +177,8 @@ public class PostService {
|
||||
}
|
||||
|
||||
private PostResponse doUpdate(Long id, PostRequest request, List<MultipartFile> images, List<String> existingUrls) {
|
||||
Post post = postRepository.findById(id)
|
||||
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "Post not found", HttpStatus.NOT_FOUND));
|
||||
// Проверка владельца выполняется до любых изменений и загрузки файлов
|
||||
Post post = findOwnedPost(id);
|
||||
|
||||
post.setStatus(PostStatus.MODERATION);
|
||||
post.setTitle(request.getTitle());
|
||||
@@ -174,31 +215,35 @@ public class PostService {
|
||||
|
||||
Post updated = postRepository.save(post);
|
||||
log.info("Обновлён пост id={}: удалено изображений={}, добавлено={}, статус={}", id, toRemove.size(), uploaded, updated.getStatus());
|
||||
|
||||
postCacheService.invalidatePosts();
|
||||
mapCacheService.clearMapMarkersCache();
|
||||
log.info("redis cache invalidated in update");
|
||||
return mapper.toResponse(updated);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long id) {
|
||||
Post post = postRepository.findById(id)
|
||||
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "Post not found", HttpStatus.NOT_FOUND));
|
||||
Post post = findOwnedPost(id);
|
||||
|
||||
// Без этого файлы останутся в MinIO мусором после удаления объявления
|
||||
post.getImages().forEach(img -> deleteFileQuietly(img.getUrl()));
|
||||
|
||||
postRepository.delete(post);
|
||||
log.info("Удалён пост id={}, изображений={}", id, post.getImages().size());
|
||||
postCacheService.invalidatePosts();
|
||||
mapCacheService.clearMapMarkersCache();
|
||||
log.info("redis cache invalidated in delete");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PostResponse updateStatus(Long id, String statusString) {
|
||||
Post post = postRepository.findById(id)
|
||||
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "Post not found", HttpStatus.NOT_FOUND));
|
||||
Post post = findOwnedPost(id);
|
||||
|
||||
User currentUser = userService.getCurrentUser();
|
||||
if (!post.getUser().getId().equals(currentUser.getId())) {
|
||||
log.warn("Попытка сменить статус чужого поста: postId={}, userId={}", id, currentUser.getId());
|
||||
throw new ApiException(ErrorCode.FORBIDDEN, "Only the post owner can update status", HttpStatus.FORBIDDEN);
|
||||
// Пост на модерации или отклонённый владелец не может сам перевести в ACTIVE:
|
||||
// иначе можно обойти модерацию (edit -> MODERATION -> updateStatus(ACTIVE))
|
||||
if (!FEED_VISIBLE_STATUSES.contains(post.getStatus())) {
|
||||
log.warn("Попытка сменить статус неопубликованного поста: postId={}, статус={}", id, post.getStatus());
|
||||
throw new ApiException(ErrorCode.FORBIDDEN, "Post is not published yet", HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
PostStatus newStatus = parseOwnerSettableStatus(statusString);
|
||||
@@ -211,6 +256,9 @@ public class PostService {
|
||||
post.setStatus(newStatus);
|
||||
Post updated = postRepository.save(post);
|
||||
log.info("Статус поста id={} изменён: {} -> {}", id, previousStatus, newStatus);
|
||||
postCacheService.invalidatePosts();
|
||||
mapCacheService.clearMapMarkersCache();
|
||||
log.info("redis cache invalidated in update status");
|
||||
return mapper.toResponse(updated);
|
||||
}
|
||||
|
||||
@@ -236,6 +284,35 @@ public class PostService {
|
||||
return postRepository.findDistinctCities(FEED_VISIBLE_STATUSES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит пост и проверяет, что текущий пользователь — его владелец.
|
||||
* NOT_FOUND, если поста нет; FORBIDDEN, если пост чужой.
|
||||
*/
|
||||
private Post findOwnedPost(Long id) {
|
||||
Post post = postRepository.findById(id)
|
||||
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "Post not found", HttpStatus.NOT_FOUND));
|
||||
|
||||
User currentUser = userService.getCurrentUser();
|
||||
if (!post.getUser().getId().equals(currentUser.getId())) {
|
||||
log.warn("Попытка изменить чужой пост: postId={}, userId={}", id, currentUser.getId());
|
||||
throw new ApiException(ErrorCode.FORBIDDEN, "Only the post owner can modify this post", HttpStatus.FORBIDDEN);
|
||||
}
|
||||
return post;
|
||||
}
|
||||
|
||||
/**
|
||||
* true, если запрос выполняет авторизованный пользователь с указанным id.
|
||||
* Для анонимного запроса возвращает false.
|
||||
*/
|
||||
private boolean isCurrentUser(Long userId) {
|
||||
try {
|
||||
User currentUser = userService.getCurrentUser();
|
||||
return currentUser != null && currentUser.getId().equals(userId);
|
||||
} catch (ApiException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private PostStatus parseOwnerSettableStatus(String statusString) {
|
||||
PostStatus newStatus;
|
||||
try {
|
||||
@@ -316,6 +393,12 @@ public class PostService {
|
||||
Post post = postRepository.findById(postId)
|
||||
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "Post not found", HttpStatus.NOT_FOUND));
|
||||
|
||||
// Неопубликованный пост (MODERATION, REJECTED) виден только владельцу.
|
||||
// Остальным отвечаем 404, чтобы не раскрывать, что такой id существует
|
||||
if (!FEED_VISIBLE_STATUSES.contains(post.getStatus()) && !isCurrentUser(post.getUser().getId())) {
|
||||
throw new ApiException(ErrorCode.NOT_FOUND, "Post not found", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
return toResponse(post, true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user