176 lines
6.1 KiB
Java
176 lines
6.1 KiB
Java
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);
|
||
}
|
||
|
||
|
||
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;
|
||
}
|
||
}
|