From 3464137511d29b975ee1d02b95b5f81375fcd82a Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:37:40 +0800 Subject: [PATCH] feat: email notifications for replies and reactions --- .../repository/ReactionRepository.java | 3 ++ .../openisle/service/NotificationService.java | 15 ++++++- .../com/openisle/service/ReactionService.java | 16 +++++++ .../service/NotificationServiceTest.java | 41 +++++++++++++++--- .../openisle/service/ReactionServiceTest.java | 43 +++++++++++++++++++ 5 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/openisle/service/ReactionServiceTest.java diff --git a/src/main/java/com/openisle/repository/ReactionRepository.java b/src/main/java/com/openisle/repository/ReactionRepository.java index c5055af55..047742120 100644 --- a/src/main/java/com/openisle/repository/ReactionRepository.java +++ b/src/main/java/com/openisle/repository/ReactionRepository.java @@ -32,4 +32,7 @@ public interface ReactionRepository extends JpaRepository { @Query("SELECT COUNT(r) FROM Reaction r WHERE r.type = com.openisle.model.ReactionType.LIKE AND ((r.post IS NOT NULL AND r.post.author.username = :username) OR (r.comment IS NOT NULL AND r.comment.author.username = :username))") long countLikesReceived(@Param("username") String username); + + @Query("SELECT COUNT(r) FROM Reaction r WHERE (r.post IS NOT NULL AND r.post.author.username = :username) OR (r.comment IS NOT NULL AND r.comment.author.username = :username)") + long countReceived(@Param("username") String username); } diff --git a/src/main/java/com/openisle/service/NotificationService.java b/src/main/java/com/openisle/service/NotificationService.java index 73ef61929..68e721887 100644 --- a/src/main/java/com/openisle/service/NotificationService.java +++ b/src/main/java/com/openisle/service/NotificationService.java @@ -4,6 +4,8 @@ import com.openisle.model.*; import com.openisle.repository.NotificationRepository; import com.openisle.repository.UserRepository; import lombok.RequiredArgsConstructor; +import com.openisle.service.EmailSender; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.List; @@ -14,6 +16,10 @@ import java.util.List; public class NotificationService { private final NotificationRepository notificationRepository; private final UserRepository userRepository; + private final EmailSender emailSender; + + @Value("${app.website-url}") + private String websiteUrl; public Notification createNotification(User user, NotificationType type, Post post, Comment comment, Boolean approved) { return createNotification(user, type, post, comment, approved, null, null, null); @@ -30,7 +36,14 @@ public class NotificationService { n.setFromUser(fromUser); n.setReactionType(reactionType); n.setContent(content); - return notificationRepository.save(n); + n = notificationRepository.save(n); + + if (type == NotificationType.COMMENT_REPLY && user.getEmail() != null && post != null && comment != null) { + String url = String.format("%s/posts/%d#comment-%d", websiteUrl, post.getId(), comment.getId()); + emailSender.sendEmail(user.getEmail(), "【OpenIsle】有人回复了你", url); + } + + return n; } /** diff --git a/src/main/java/com/openisle/service/ReactionService.java b/src/main/java/com/openisle/service/ReactionService.java index 371028853..b752b828a 100644 --- a/src/main/java/com/openisle/service/ReactionService.java +++ b/src/main/java/com/openisle/service/ReactionService.java @@ -11,7 +11,9 @@ import com.openisle.repository.PostRepository; import com.openisle.repository.ReactionRepository; import com.openisle.repository.UserRepository; import com.openisle.service.NotificationService; +import com.openisle.service.EmailSender; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service @@ -22,6 +24,10 @@ public class ReactionService { private final PostRepository postRepository; private final CommentRepository commentRepository; private final NotificationService notificationService; + private final EmailSender emailSender; + + @Value("${app.website-url}") + private String websiteUrl; public Reaction reactToPost(String username, Long postId, ReactionType type) { User user = userRepository.findByUsername(username) @@ -41,6 +47,11 @@ public class ReactionService { reaction = reactionRepository.save(reaction); if (!user.getId().equals(post.getAuthor().getId())) { notificationService.createNotification(post.getAuthor(), NotificationType.REACTION, post, null, null, user, type, null); + long count = reactionRepository.countReceived(post.getAuthor().getUsername()); + if (count % 5 == 0 && post.getAuthor().getEmail() != null) { + String url = websiteUrl + "/messages"; + emailSender.sendEmail(post.getAuthor().getEmail(), "【OpenIsle】你有新的互动", url); + } } return reaction; } @@ -64,6 +75,11 @@ public class ReactionService { reaction = reactionRepository.save(reaction); if (!user.getId().equals(comment.getAuthor().getId())) { notificationService.createNotification(comment.getAuthor(), NotificationType.REACTION, comment.getPost(), comment, null, user, type, null); + long count = reactionRepository.countReceived(comment.getAuthor().getUsername()); + if (count % 5 == 0 && comment.getAuthor().getEmail() != null) { + String url = websiteUrl + "/messages"; + emailSender.sendEmail(comment.getAuthor().getEmail(), "【OpenIsle】你有新的互动", url); + } } return reaction; } diff --git a/src/test/java/com/openisle/service/NotificationServiceTest.java b/src/test/java/com/openisle/service/NotificationServiceTest.java index 508915d32..82bcd12e3 100644 --- a/src/test/java/com/openisle/service/NotificationServiceTest.java +++ b/src/test/java/com/openisle/service/NotificationServiceTest.java @@ -18,7 +18,9 @@ class NotificationServiceTest { void markReadUpdatesOnlyOwnedNotifications() { NotificationRepository nRepo = mock(NotificationRepository.class); UserRepository uRepo = mock(UserRepository.class); - NotificationService service = new NotificationService(nRepo, uRepo); + EmailSender email = mock(EmailSender.class); + NotificationService service = new NotificationService(nRepo, uRepo, email); + org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); User user = new User(); user.setId(1L); @@ -44,7 +46,9 @@ class NotificationServiceTest { void listNotificationsWithoutFilter() { NotificationRepository nRepo = mock(NotificationRepository.class); UserRepository uRepo = mock(UserRepository.class); - NotificationService service = new NotificationService(nRepo, uRepo); + EmailSender email = mock(EmailSender.class); + NotificationService service = new NotificationService(nRepo, uRepo, email); + org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); User user = new User(); user.setId(2L); @@ -64,7 +68,9 @@ class NotificationServiceTest { void countUnreadReturnsRepositoryValue() { NotificationRepository nRepo = mock(NotificationRepository.class); UserRepository uRepo = mock(UserRepository.class); - NotificationService service = new NotificationService(nRepo, uRepo); + EmailSender email = mock(EmailSender.class); + NotificationService service = new NotificationService(nRepo, uRepo, email); + org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); User user = new User(); user.setId(3L); @@ -82,7 +88,9 @@ class NotificationServiceTest { void createRegisterRequestNotificationsDeletesOldOnes() { NotificationRepository nRepo = mock(NotificationRepository.class); UserRepository uRepo = mock(UserRepository.class); - NotificationService service = new NotificationService(nRepo, uRepo); + EmailSender email = mock(EmailSender.class); + NotificationService service = new NotificationService(nRepo, uRepo, email); + org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); User admin = new User(); admin.setId(10L); @@ -101,7 +109,9 @@ class NotificationServiceTest { void createActivityRedeemNotificationsDeletesOldOnes() { NotificationRepository nRepo = mock(NotificationRepository.class); UserRepository uRepo = mock(UserRepository.class); - NotificationService service = new NotificationService(nRepo, uRepo); + EmailSender email = mock(EmailSender.class); + NotificationService service = new NotificationService(nRepo, uRepo, email); + org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); User admin = new User(); admin.setId(10L); @@ -115,4 +125,25 @@ class NotificationServiceTest { verify(nRepo).deleteByTypeAndFromUser(NotificationType.ACTIVITY_REDEEM, user); verify(nRepo).save(any(Notification.class)); } + + @Test + void createNotificationSendsEmailForCommentReply() { + NotificationRepository nRepo = mock(NotificationRepository.class); + UserRepository uRepo = mock(UserRepository.class); + EmailSender email = mock(EmailSender.class); + NotificationService service = new NotificationService(nRepo, uRepo, email); + org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); + + User user = new User(); + user.setEmail("a@a.com"); + Post post = new Post(); + post.setId(1L); + Comment comment = new Comment(); + comment.setId(2L); + when(nRepo.save(any(Notification.class))).thenAnswer(i -> i.getArgument(0)); + + service.createNotification(user, NotificationType.COMMENT_REPLY, post, comment, null, null, null, null); + + verify(email).sendEmail("a@a.com", "【OpenIsle】有人回复了你", "https://ex.com/posts/1#comment-2"); + } } diff --git a/src/test/java/com/openisle/service/ReactionServiceTest.java b/src/test/java/com/openisle/service/ReactionServiceTest.java new file mode 100644 index 000000000..fea2e9fd9 --- /dev/null +++ b/src/test/java/com/openisle/service/ReactionServiceTest.java @@ -0,0 +1,43 @@ +package com.openisle.service; + +import com.openisle.model.*; +import com.openisle.repository.*; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.mockito.Mockito.*; + +class ReactionServiceTest { + @Test + void reactToPostSendsEmailEveryFive() { + ReactionRepository reactionRepo = mock(ReactionRepository.class); + UserRepository userRepo = mock(UserRepository.class); + PostRepository postRepo = mock(PostRepository.class); + CommentRepository commentRepo = mock(CommentRepository.class); + NotificationService notif = mock(NotificationService.class); + EmailSender email = mock(EmailSender.class); + ReactionService service = new ReactionService(reactionRepo, userRepo, postRepo, commentRepo, notif, email); + org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com"); + + User user = new User(); + user.setId(1L); + user.setUsername("bob"); + User author = new User(); + author.setId(2L); + author.setEmail("a@a.com"); + Post post = new Post(); + post.setId(3L); + post.setAuthor(author); + + when(userRepo.findByUsername("bob")).thenReturn(Optional.of(user)); + when(postRepo.findById(3L)).thenReturn(Optional.of(post)); + when(reactionRepo.findByUserAndPostAndType(user, post, ReactionType.LIKE)).thenReturn(Optional.empty()); + when(reactionRepo.save(any(Reaction.class))).thenAnswer(i -> i.getArgument(0)); + when(reactionRepo.countReceived(author.getUsername())).thenReturn(5L); + + service.reactToPost("bob", 3L, ReactionType.LIKE); + + verify(email).sendEmail("a@a.com", "【OpenIsle】你有新的互动", "https://ex.com/messages"); + } +}