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 getUserChats(Long userId) { return chatRepository.findAllByUserId(userId).stream() .map(c -> toDto(c, userId)) .collect(Collectors.toList()); } // История сообщений в чате, с пагинацией и проверкой доступа @Transactional(readOnly = true) public Page 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; } }