Files
nahodka-back/src/main/java/com/krylov/refound/service/ChatService.java
2026-09-30 00:27:10 +03:00

188 lines
6.6 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.krylov.refound.service;
import com.krylov.refound.dto.ChatDto;
import com.krylov.refound.dto.ChatPostDto;
import com.krylov.refound.dto.MessageDto;
import com.krylov.refound.entity.Chat;
import com.krylov.refound.entity.Image;
import com.krylov.refound.entity.Message;
import com.krylov.refound.repository.ChatRepository;
import com.krylov.refound.repository.MessageRepository;
import com.krylov.refound.repository.PostRepository;
import com.krylov.refound.repository.UserRepository;
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 PostRepository postRepository;
// Создать чат с пользователем (или вернуть существующий)
@Transactional
public ChatDto getOrCreateChat(Long currentUserId, Long otherUserId, Long postId) {
if (currentUserId.equals(otherUserId)) {
throw new IllegalArgumentException("Нельзя создать чат с самим собой");
}
Long userId1 = Math.min(currentUserId, otherUserId);
Long userId2 = Math.max(currentUserId, otherUserId);
Chat chat = chatRepository.findByUsers(userId1, userId2).orElseGet(() -> {
Chat newChat = new Chat();
newChat.setUserOneId(userId1);
newChat.setUserTwoId(userId2);
newChat.setPostId(postId);
return chatRepository.save(newChat);
});
return toDto(chat, currentUserId);
}
// Все чаты пользователя
@Transactional(readOnly = true)
public List<ChatDto> getUserChats(Long userId) {
return chatRepository.findAllByUserId(userId).stream()
.map(c -> toDto(c, userId))
.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 void sendMessage(Long senderId, Long chatId, String content) {
Chat chat = getChatOrThrow(chatId);
checkParticipant(chat, senderId);
Message message = new Message();
message.setChatId(chatId);
message.setSenderId(senderId);
message.setContent(content);
message = messageRepository.save(message);
MessageDto dto = toMessageDto(message);
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)
);
}
@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);
}
/**
* Проверка для STOMP-интерцептора: пользователь состоит в чате или нет.
* Не бросает исключений — вызывается на этапе авторизации подписки.
*/
@Transactional(readOnly = true)
public boolean isParticipant(Long chatId, Long userId) {
if (chatId == null || userId == null) {
return false;
}
return chatRepository.isParticipantOfChat(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();
ChatDto dto = new ChatDto();
dto.setId(chat.getId());
dto.setOtherUserId(otherUserId);
dto.setPostId(chat.getPostId());
userRepository.findById(otherUserId).ifPresent(user -> {
dto.setOtherUserName(user.getName() + " " + user.getLastName());
dto.setOtherUserAvatar(user.getAvatarUrl());
});
if (chat.getPostId() != null) {
postRepository.findById(chat.getPostId()).ifPresent(post -> {
ChatPostDto postDto = new ChatPostDto();
postDto.setId(post.getId());
postDto.setTitle(post.getTitle());
postDto.setImages(post.getImages().stream().map(Image::getUrl).toList());
dto.setPost(postDto);
});
}
// без N+1: берём только последнее сообщение, а не всю историю
messageRepository.findFirstByChatIdOrderByCreatedAtDesc(chat.getId())
.ifPresent(last -> {
dto.setLastMessage(last.getContent());
dto.setLastMessageAt(last.getCreatedAt());
});
dto.setUnreadCount(
messageRepository.countByChatIdAndIsReadFalseAndSenderIdNot(chat.getId(), currentUserId)
);
return dto;
}
private MessageDto toMessageDto(Message m) {
MessageDto dto = new MessageDto();
dto.setId(m.getId());
dto.setChatId(m.getChatId());
dto.setSenderId(m.getSenderId());
dto.setContent(m.getContent());
dto.setRead(m.isRead());
dto.setCreatedAt(m.getCreatedAt());
return dto;
}
}