added filter on title and description on post
This commit is contained in:
@@ -5,6 +5,7 @@ import com.krylov.refound.service.ChatService;
|
|||||||
import com.krylov.refound.service.UserService;
|
import com.krylov.refound.service.UserService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -42,4 +43,17 @@ public class ChatController {
|
|||||||
return userService.getUserIdByLogin(login);
|
return userService.getUserIdByLogin(login);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{chatId}")
|
||||||
|
public void deleteChat(@PathVariable Long chatId, Authentication auth) {
|
||||||
|
Long currentUserId = extractUserId(auth);
|
||||||
|
chatService.deleteChat(chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/api/v1/chats/{chatId}/read")
|
||||||
|
public void markAsRead(@PathVariable Long chatId) {
|
||||||
|
Long userId = (Long) SecurityContextHolder.getContext()
|
||||||
|
.getAuthentication().getPrincipal(); // или твой способ
|
||||||
|
chatService.markAsRead(chatId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import java.util.List;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
@@ -44,8 +48,10 @@ public class PostController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<PostResponse> getFeed() {
|
public Page<PostResponse> getFeed(
|
||||||
return service.getFeed();
|
@RequestParam(required = false) String search,
|
||||||
|
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
|
||||||
|
return service.getFeed(search, pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ import java.util.List;
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Modifying;
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
public interface MessageRepository extends JpaRepository<Message, Long> {
|
public interface MessageRepository extends JpaRepository<Message, Long> {
|
||||||
|
|
||||||
List<Message> findByChatIdOrderByCreatedAtAsc(Long chatId);
|
List<Message> findByChatIdOrderByCreatedAtAsc(Long chatId);
|
||||||
long countByChatIdAndIsReadFalseAndSenderIdNot(Long chatId, Long currentUserId);
|
long countByChatIdAndIsReadFalseAndSenderIdNot(Long chatId, Long currentUserId);
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE Message m SET m.isRead = true WHERE m.chatId = :chatId AND m.senderId != :userId")
|
||||||
|
int markAsReadByChatIdAndRecipient(@Param("chatId") Long chatId, @Param("userId") Long userId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.krylov.refound.repository.ChatRepository;
|
|||||||
import com.krylov.refound.repository.MessageRepository;
|
import com.krylov.refound.repository.MessageRepository;
|
||||||
import com.krylov.refound.repository.UserRepository;
|
import com.krylov.refound.repository.UserRepository;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
@@ -64,7 +65,6 @@ public class ChatService {
|
|||||||
Chat chat = chatRepository.findById(chatId)
|
Chat chat = chatRepository.findById(chatId)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Чат не найден"));
|
.orElseThrow(() -> new IllegalArgumentException("Чат не найден"));
|
||||||
|
|
||||||
// Проверка что отправитель - участник чата
|
|
||||||
if (!chat.getUserOneId().equals(senderId) && !chat.getUserTwoId().equals(senderId)) {
|
if (!chat.getUserOneId().equals(senderId) && !chat.getUserTwoId().equals(senderId)) {
|
||||||
throw new SecurityException("Вы не участник этого чата");
|
throw new SecurityException("Вы не участник этого чата");
|
||||||
}
|
}
|
||||||
@@ -77,9 +77,21 @@ public class ChatService {
|
|||||||
|
|
||||||
MessageDto dto = toMessageDto(message);
|
MessageDto dto = toMessageDto(message);
|
||||||
|
|
||||||
// Отправляем сообщение через WebSocket в топик конкретного чата
|
|
||||||
messagingTemplate.convertAndSend("/topic/chat/" + chatId, dto);
|
messagingTemplate.convertAndSend("/topic/chat/" + chatId, dto);
|
||||||
|
|
||||||
|
// Уведомляем получателя об непрочитанных
|
||||||
|
Long recipientId = chat.getUserOneId().equals(senderId)
|
||||||
|
? chat.getUserTwoId()
|
||||||
|
: chat.getUserOneId();
|
||||||
|
|
||||||
|
long unreadCount = messageRepository
|
||||||
|
.countByChatIdAndIsReadFalseAndSenderIdNot(chatId, recipientId);
|
||||||
|
|
||||||
|
messagingTemplate.convertAndSend(
|
||||||
|
"/topic/user/" + recipientId + "/unread",
|
||||||
|
Map.of("chatId", chatId, "unreadCount", unreadCount)
|
||||||
|
);
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,4 +134,13 @@ public class ChatService {
|
|||||||
dto.setCreatedAt(m.getCreatedAt());
|
dto.setCreatedAt(m.getCreatedAt());
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void deleteChat(Long chatId) {
|
||||||
|
chatRepository.deleteById(chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void markAsRead(Long chatId, Long userId) {
|
||||||
|
messageRepository.markAsReadByChatIdAndRecipient(chatId, userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.krylov.refound.mapper.PostMapper;
|
|||||||
import com.krylov.refound.repository.FavoriteRepository;
|
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.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -28,6 +29,9 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
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.data.jpa.domain.Specification;
|
||||||
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;
|
||||||
@@ -83,22 +87,24 @@ public class PostService {
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<PostResponse> getFeed() {
|
public Page<PostResponse> getFeed(String search, Pageable pageable) {
|
||||||
List<Post> posts = repository.findAll();
|
Specification<Post> spec = PostSpecification.hasSearch(search);
|
||||||
|
Page<Post> postsPage = repository.findAll(spec, pageable);
|
||||||
|
|
||||||
// Get all post IDs
|
List<Long> postIds = postsPage.getContent().stream()
|
||||||
List<Long> postIds = posts.stream()
|
|
||||||
.map(Post::getId)
|
.map(Post::getId)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Count likes for each post
|
if (postIds.isEmpty()) {
|
||||||
|
return Page.empty(pageable);
|
||||||
|
}
|
||||||
|
|
||||||
Map<Long, Long> likeCounts = favoriteRepository.countLikesByPostIds(postIds).stream()
|
Map<Long, Long> likeCounts = favoriteRepository.countLikesByPostIds(postIds).stream()
|
||||||
.collect(Collectors.toMap(
|
.collect(Collectors.toMap(
|
||||||
row -> (Long) row[0], // post ID
|
row -> (Long) row[0],
|
||||||
row -> (Long) row[1] // like count
|
row -> (Long) row[1]
|
||||||
));
|
));
|
||||||
|
|
||||||
// Get current user's favorites if authenticated
|
|
||||||
Set<Long> userFavoriteIds = new HashSet<>();
|
Set<Long> userFavoriteIds = new HashSet<>();
|
||||||
try {
|
try {
|
||||||
User currentUser = userService.getCurrentUser();
|
User currentUser = userService.getCurrentUser();
|
||||||
@@ -109,18 +115,14 @@ public class PostService {
|
|||||||
// User not authenticated - that's OK
|
// User not authenticated - that's OK
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set like counts and favorite status
|
|
||||||
final Set<Long> finalUserFavoriteIds = userFavoriteIds;
|
final Set<Long> finalUserFavoriteIds = userFavoriteIds;
|
||||||
return posts.stream()
|
return postsPage.map(post -> {
|
||||||
.map(post -> {
|
PostResponse response = mapper.toResponse(post);
|
||||||
PostResponse response = mapper.toResponse(post);
|
response.setLikeCount(likeCounts.getOrDefault(post.getId(), 0L));
|
||||||
response.setLikeCount(likeCounts.getOrDefault(post.getId(), 0L));
|
response.setFavorite(finalUserFavoriteIds.contains(post.getId()));
|
||||||
response.setFavorite(finalUserFavoriteIds.contains(post.getId()));
|
response.setUserRole(post.getUser().getRole());
|
||||||
response.setUserRole(post.getUser().getRole());
|
return response;
|
||||||
return response;
|
});
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List <PostResponse> getById(Long id) {
|
public List <PostResponse> getById(Long id) {
|
||||||
|
|||||||
@@ -1,58 +1,26 @@
|
|||||||
package com.krylov.refound.util;
|
package com.krylov.refound.util;
|
||||||
|
|
||||||
import com.krylov.refound.dto.PostFilter;
|
|
||||||
import com.krylov.refound.entity.Post;
|
import com.krylov.refound.entity.Post;
|
||||||
import jakarta.persistence.criteria.Expression;
|
|
||||||
import jakarta.persistence.criteria.Predicate;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
|
||||||
public class PostSpecification {
|
public class PostSpecification {
|
||||||
public static Specification<Post> withFilter(PostFilter filter) {
|
public static Specification<Post> hasSearch(String search) {
|
||||||
return (root, query, cb) -> {
|
return (root, query, cb) -> {
|
||||||
|
if (search == null || search.isBlank()) {
|
||||||
List<Predicate> predicates = new ArrayList<>();
|
return cb.conjunction();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.getType() != null) {
|
|
||||||
predicates.add(cb.equal(root.get("type"), filter.getType()));
|
|
||||||
}
|
}
|
||||||
|
String pattern = "%" + search.toLowerCase() + "%";
|
||||||
|
return cb.or(
|
||||||
|
cb.like(cb.lower(root.get("title")), pattern),
|
||||||
|
cb.like(cb.lower(root.get("description")), pattern)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (filter.getCity() != null) {
|
public static Specification<Post> hasType(String type) {
|
||||||
predicates.add(cb.equal(root.get("city"), filter.getCity()));
|
return (root, query, cb) -> {
|
||||||
}
|
if (type == null || type.isBlank()) return cb.conjunction();
|
||||||
|
return cb.equal(cb.lower(root.get("type")), type.toLowerCase());
|
||||||
if (filter.getCategory() != null) {
|
|
||||||
predicates.add(cb.equal(root.get("category"), filter.getCategory()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.getDistrict() != null) {
|
|
||||||
predicates.add(cb.equal(root.get("district"), filter.getDistrict()));
|
|
||||||
}
|
|
||||||
if (filter.getSearch() != null && !filter.getSearch().isBlank()) {
|
|
||||||
String search = "%" + filter.getSearch().toLowerCase() + "%";
|
|
||||||
|
|
||||||
predicates.add(cb.or(
|
|
||||||
cb.like(cb.lower(root.get("title")), search),
|
|
||||||
cb.like(cb.lower(root.get("description")), search)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (filter.getLat() != null && filter.getLng() != null && filter.getRadius() != null) {
|
|
||||||
|
|
||||||
Expression<Double> distance = cb.function(
|
|
||||||
"6371 * acos(cos(radians(?1)) * cos(radians(latitude)) * " +
|
|
||||||
"cos(radians(longitude) - radians(?2)) + sin(radians(?1)) * sin(radians(latitude)))",
|
|
||||||
Double.class,
|
|
||||||
cb.literal(filter.getLat()),
|
|
||||||
cb.literal(filter.getLng())
|
|
||||||
);
|
|
||||||
|
|
||||||
predicates.add(cb.lessThanOrEqualTo(distance, filter.getRadius()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return cb.and(predicates.toArray(new Predicate[0]));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user