added EP rejected posts for admin

This commit is contained in:
SlimusMinus
2026-10-06 02:08:28 +03:00
parent 7adc4f28f1
commit 1a7b96e8a6
3 changed files with 55 additions and 4 deletions

View File

@@ -38,6 +38,7 @@ public class PostAdminController {
private final AdminPostService adminPostService; private final AdminPostService adminPostService;
/** Основная лента: только ACTIVE и CLOSED. */
@GetMapping @GetMapping
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Page<AdminPostResponse>> getPosts( public ResponseEntity<Page<AdminPostResponse>> getPosts(
@@ -51,6 +52,20 @@ public class PostAdminController {
return ResponseEntity.ok(adminPostService.getPosts(search, type, status, pageable)); return ResponseEntity.ok(adminPostService.getPosts(search, type, status, pageable));
} }
/** Отклонённые объявления: те же фильтры, но статус зафиксирован на REJECTED. */
@GetMapping("/rejected")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Page<AdminPostResponse>> getRejectedPosts(
@RequestParam(required = false) String search,
@RequestParam(required = false) String type,
@RequestParam(required = false) String status,
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC)
Pageable pageable
) {
log.info("getRejectedPosts search={} type={} status={} page={}", search, type, status, pageable.getPageNumber());
return ResponseEntity.ok(adminPostService.getRejectedPosts(search, type, status, pageable));
}
/** Multipart-поля те же, что у пользовательского PUT /api/v1/posts/{id}. */ /** Multipart-поля те же, что у пользовательского PUT /api/v1/posts/{id}. */
@PutMapping(value = "/{postId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PutMapping(value = "/{postId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasRole('ADMIN')")

View File

@@ -23,9 +23,11 @@ import com.krylov.refound.service.redis.MapCacheService;
import com.krylov.refound.service.redis.PostCacheService; import com.krylov.refound.service.redis.PostCacheService;
import com.krylov.refound.util.PostSpecification; import com.krylov.refound.util.PostSpecification;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -58,6 +60,12 @@ public class AdminPostService {
private static final int MAX_PAGE_SIZE = 100; private static final int MAX_PAGE_SIZE = 100;
/** Основная админская лента: опубликованные объявления, включая закрытые. */
private static final Set<PostStatus> FEED_STATUSES = EnumSet.of(PostStatus.ACTIVE, PostStatus.CLOSED);
/** Отклонённые объявления живут отдельной лентой, в основной их не подмешиваем. */
private static final Set<PostStatus> REJECTED_STATUSES = EnumSet.of(PostStatus.REJECTED);
private final PostRepository postRepository; private final PostRepository postRepository;
private final ImageRepository imageRepository; private final ImageRepository imageRepository;
private final FavoriteRepository favoriteRepository; private final FavoriteRepository favoriteRepository;
@@ -70,13 +78,34 @@ public class AdminPostService {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
/** /**
* Лента объявлений: видны все статусы, включая MODERATION и REJECTED. * Основная лента объявлений: только ACTIVE и CLOSED. MODERATION и REJECTED
* доступны через {@link #getRejectedPosts(String, String, String, Pageable)}.
* Фильтры приходят строками и разбираются вручную — неверное значение даёт 400 вместо 500. * Фильтры приходят строками и разбираются вручную — неверное значение даёт 400 вместо 500.
*/ */
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Page<AdminPostResponse> getPosts(String search, String rawType, String rawStatus, Pageable pageable) { public Page<AdminPostResponse> getPosts(String search, String rawType, String rawStatus, Pageable pageable) {
return searchPosts(search, rawType, rawStatus, FEED_STATUSES, pageable);
}
/** Отклонённые объявления: те же фильтры, но статус зафиксирован на REJECTED. */
@Transactional(readOnly = true)
public Page<AdminPostResponse> getRejectedPosts(String search, String rawType, String rawStatus,
Pageable pageable) {
return searchPosts(search, rawType, rawStatus, REJECTED_STATUSES, pageable);
}
private Page<AdminPostResponse> searchPosts(String search, String rawType, String rawStatus,
Set<PostStatus> allowedStatuses, Pageable pageable) {
PostStatus status = parseStatus(rawStatus);
if (status != null && !allowedStatuses.contains(status)) {
throw new ApiException(ErrorCode.VALIDATION_ERROR,
"Статус " + status + " недоступен в этой ленте, допустимы: " + allowedStatuses,
HttpStatus.BAD_REQUEST);
}
Page<Post> page = postRepository.findAll( Page<Post> page = postRepository.findAll(
PostSpecification.adminSearch(search, parseType(rawType), parseStatus(rawStatus)), PostSpecification.adminSearch(search, parseType(rawType), status, allowedStatuses),
safePageable(pageable) safePageable(pageable)
); );

View File

@@ -5,18 +5,25 @@ import com.krylov.refound.entity.User;
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.criteria.Join; import jakarta.persistence.criteria.Join;
import java.util.Set;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
public class PostSpecification { public class PostSpecification {
/** /**
* Поиск для админской ленты: видимы все объявления, включая MODERATION и REJECTED. * Поиск для админской ленты: видны только статусы из {@code allowedStatuses},
* параметр {@code status} сужает выборку внутри них. {@code allowedStatuses == null}
* снимает ограничение по статусу (все объявления, включая MODERATION и REJECTED).
* Совпадение ищется по названию, описанию, городу, району и данным владельца. * Совпадение ищется по названию, описанию, городу, району и данным владельца.
*/ */
public static Specification<Post> adminSearch(String search, PostType type, PostStatus status) { public static Specification<Post> adminSearch(
String search, PostType type, PostStatus status, Set<PostStatus> allowedStatuses) {
return Specification.allOf( return Specification.allOf(
(root, query, cb) -> type == null ? cb.conjunction() : cb.equal(root.get("type"), type), (root, query, cb) -> type == null ? cb.conjunction() : cb.equal(root.get("type"), type),
(root, query, cb) -> allowedStatuses == null || allowedStatuses.isEmpty()
? cb.conjunction()
: root.get("status").in(allowedStatuses),
(root, query, cb) -> status == null ? cb.conjunction() : cb.equal(root.get("status"), status), (root, query, cb) -> status == null ? cb.conjunction() : cb.equal(root.get("status"), status),
(root, query, cb) -> { (root, query, cb) -> {
if (!StringUtils.hasText(search)) { if (!StringUtils.hasText(search)) {