From 69f5745fe86030eb27711219e69c9b5008dacdea Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 30 Jul 2025 10:42:06 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E9=82=AE=E4=BB=B6=E5=8F=91?= =?UTF-8?q?=E9=80=81=EF=BC=8C=E4=BF=AE=E6=94=B9=E4=B8=BA=E4=B8=AD=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/openisle/controller/AdminUserController.java | 8 ++++---- src/main/java/com/openisle/controller/AuthController.java | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/openisle/controller/AdminUserController.java b/src/main/java/com/openisle/controller/AdminUserController.java index cd72af585..e82e5a7e2 100644 --- a/src/main/java/com/openisle/controller/AdminUserController.java +++ b/src/main/java/com/openisle/controller/AdminUserController.java @@ -22,8 +22,8 @@ public class AdminUserController { User user = userRepository.findById(id).orElseThrow(); user.setApproved(true); userRepository.save(user); - emailSender.sendEmail(user.getEmail(), "Registration Approved", - "Your account has been approved. Visit: " + websiteUrl); + emailSender.sendEmail(user.getEmail(), "【OpenIsle】您的注册已审核通过", + "🎉您的注册已经审核通过, 点击以访问网站: " + websiteUrl); return ResponseEntity.ok().build(); } @@ -32,8 +32,8 @@ public class AdminUserController { User user = userRepository.findById(id).orElseThrow(); user.setApproved(false); userRepository.save(user); - emailSender.sendEmail(user.getEmail(), "Registration Rejected", - "Your account request was rejected. Visit: " + websiteUrl); + emailSender.sendEmail(user.getEmail(), "【OpenIsle】您的注册已被管理员拒绝", + "您的注册被管理员拒绝, 点击链接可以重新填写理由申请: " + websiteUrl); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/openisle/controller/AuthController.java b/src/main/java/com/openisle/controller/AuthController.java index e824758ba..f8f6181c2 100644 --- a/src/main/java/com/openisle/controller/AuthController.java +++ b/src/main/java/com/openisle/controller/AuthController.java @@ -56,7 +56,7 @@ public class AuthController { } User user = userService.register( req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode()); - emailService.sendEmail(user.getEmail(), "Verification Code", "Your verification code is " + user.getVerificationCode()); + emailService.sendEmail(user.getEmail(), "【OpenIsle】在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode()); if (!user.isApproved()) { notificationService.createRegisterRequestNotifications(user, user.getRegisterReason()); } @@ -92,7 +92,7 @@ public class AuthController { User user = userOpt.get(); if (!user.isVerified()) { user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode()); - emailService.sendEmail(user.getEmail(), "Verification Code", "Your verification code is " + user.getVerificationCode()); + emailService.sendEmail(user.getEmail(), "【OpenIsle】在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode()); return ResponseEntity.badRequest().body(Map.of( "error", "User not verified", "reason_code", "NOT_VERIFIED", @@ -279,7 +279,7 @@ public class AuthController { return ResponseEntity.badRequest().body(Map.of("error", "User not found")); } String code = userService.generatePasswordResetCode(req.getEmail()); - emailService.sendEmail(req.getEmail(), "Password Reset Code", "Your verification code is " + code); + emailService.sendEmail(req.getEmail(), "【OpenIsle】请填写验证码以重置密码", "您的验证码是" + code); return ResponseEntity.ok(Map.of("message", "Verification code sent")); } 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 2/2] 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"); + } +}