From 28e3ebb911e68463570fdcc45bd1d226b6fd7d7b Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Wed, 17 Sep 2025 12:29:09 +0800 Subject: [PATCH] Handle point history cleanup when deleting posts --- .../repository/PointHistoryRepository.java | 9 +- .../com/openisle/service/PostService.java | 26 +++++- .../com/openisle/service/PostServiceTest.java | 90 ++++++++++++++++++- 3 files changed, 115 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/com/openisle/repository/PointHistoryRepository.java b/backend/src/main/java/com/openisle/repository/PointHistoryRepository.java index 2e3d6647e..876b183d1 100644 --- a/backend/src/main/java/com/openisle/repository/PointHistoryRepository.java +++ b/backend/src/main/java/com/openisle/repository/PointHistoryRepository.java @@ -1,8 +1,9 @@ package com.openisle.repository; -import com.openisle.model.PointHistory; -import com.openisle.model.User; import com.openisle.model.Comment; +import com.openisle.model.PointHistory; +import com.openisle.model.Post; +import com.openisle.model.User; import org.springframework.data.jpa.repository.JpaRepository; import java.time.LocalDateTime; @@ -14,6 +15,8 @@ public interface PointHistoryRepository extends JpaRepository findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt); - + List findByComment(Comment comment); + + List findByPost(Post post); } diff --git a/backend/src/main/java/com/openisle/service/PostService.java b/backend/src/main/java/com/openisle/service/PostService.java index 36796e346..afba1b87e 100644 --- a/backend/src/main/java/com/openisle/service/PostService.java +++ b/backend/src/main/java/com/openisle/service/PostService.java @@ -9,14 +9,12 @@ import com.openisle.repository.PollPostRepository; import com.openisle.repository.UserRepository; import com.openisle.repository.CategoryRepository; import com.openisle.repository.TagRepository; -import com.openisle.service.SubscriptionService; -import com.openisle.service.CommentService; -import com.openisle.service.PostChangeLogService; import com.openisle.repository.CommentRepository; import com.openisle.repository.ReactionRepository; import com.openisle.repository.PostSubscriptionRepository; import com.openisle.repository.NotificationRepository; import com.openisle.repository.PollVoteRepository; +import com.openisle.repository.PointHistoryRepository; import com.openisle.exception.RateLimitException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -74,6 +72,7 @@ public class PostService { private final ApplicationContext applicationContext; private final PointService pointService; private final PostChangeLogService postChangeLogService; + private final PointHistoryRepository pointHistoryRepository; private final ConcurrentMap> scheduledFinalizations = new ConcurrentHashMap<>(); @Value("${app.website-url:https://www.open-isle.com}") private String websiteUrl; @@ -102,6 +101,7 @@ public class PostService { ApplicationContext applicationContext, PointService pointService, PostChangeLogService postChangeLogService, + PointHistoryRepository pointHistoryRepository, @Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode, RedisTemplate redisTemplate) { this.postRepository = postRepository; @@ -125,6 +125,7 @@ public class PostService { this.applicationContext = applicationContext; this.pointService = pointService; this.postChangeLogService = postChangeLogService; + this.pointHistoryRepository = pointHistoryRepository; this.publishMode = publishMode; this.redisTemplate = redisTemplate; @@ -861,6 +862,25 @@ public class PostService { notificationRepository.deleteAll(notificationRepository.findByPost(post)); postReadService.deleteByPost(post); imageUploader.removeReferences(imageUploader.extractUrls(post.getContent())); + List pointHistories = pointHistoryRepository.findByPost(post); + Set usersToRecalculate = pointHistories.stream() + .map(PointHistory::getUser) + .collect(Collectors.toSet()); + if (!pointHistories.isEmpty()) { + LocalDateTime deletedAt = LocalDateTime.now(); + for (PointHistory history : pointHistories) { + history.setDeletedAt(deletedAt); + history.setPost(null); + } + pointHistoryRepository.saveAll(pointHistories); + } + if (!usersToRecalculate.isEmpty()) { + for (User affected : usersToRecalculate) { + int newPoints = pointService.recalculateUserPoints(affected); + affected.setPoint(newPoints); + } + userRepository.saveAll(usersToRecalculate); + } if (post instanceof LotteryPost lp) { ScheduledFuture future = scheduledFinalizations.remove(lp.getId()); if (future != null) { diff --git a/backend/src/test/java/com/openisle/service/PostServiceTest.java b/backend/src/test/java/com/openisle/service/PostServiceTest.java index 998fe0175..46efaa5c0 100644 --- a/backend/src/test/java/com/openisle/service/PostServiceTest.java +++ b/backend/src/test/java/com/openisle/service/PostServiceTest.java @@ -10,8 +10,11 @@ import org.springframework.data.redis.core.RedisTemplate; import static org.junit.jupiter.api.Assertions.*; -import java.util.Optional; +import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; + +import org.mockito.ArgumentCaptor; import static org.mockito.Mockito.*; @@ -39,12 +42,14 @@ class PostServiceTest { ApplicationContext context = mock(ApplicationContext.class); PointService pointService = mock(PointService.class); PostChangeLogService postChangeLogService = mock(PostChangeLogService.class); + PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class); RedisTemplate redisTemplate = mock(RedisTemplate.class); PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, reactionRepo, subRepo, notificationRepo, postReadService, - imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate); + imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, + pointHistoryRepository, PublishMode.DIRECT, redisTemplate); when(context.getBean(PostService.class)).thenReturn(service); Post post = new Post(); @@ -60,6 +65,7 @@ class PostServiceTest { when(reactionRepo.findByPost(post)).thenReturn(List.of()); when(subRepo.findByPost(post)).thenReturn(List.of()); when(notificationRepo.findByPost(post)).thenReturn(List.of()); + when(pointHistoryRepository.findByPost(post)).thenReturn(List.of()); service.deletePost(1L, "alice"); @@ -90,12 +96,14 @@ class PostServiceTest { ApplicationContext context = mock(ApplicationContext.class); PointService pointService = mock(PointService.class); PostChangeLogService postChangeLogService = mock(PostChangeLogService.class); + PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class); RedisTemplate redisTemplate = mock(RedisTemplate.class); PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, reactionRepo, subRepo, notificationRepo, postReadService, - imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate); + imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, + pointHistoryRepository, PublishMode.DIRECT, redisTemplate); when(context.getBean(PostService.class)).thenReturn(service); Post post = new Post(); @@ -117,6 +125,7 @@ class PostServiceTest { when(reactionRepo.findByPost(post)).thenReturn(List.of()); when(subRepo.findByPost(post)).thenReturn(List.of()); when(notificationRepo.findByPost(post)).thenReturn(List.of()); + when(pointHistoryRepository.findByPost(post)).thenReturn(List.of()); service.deletePost(1L, "admin"); @@ -147,12 +156,14 @@ class PostServiceTest { ApplicationContext context = mock(ApplicationContext.class); PointService pointService = mock(PointService.class); PostChangeLogService postChangeLogService = mock(PostChangeLogService.class); + PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class); RedisTemplate redisTemplate = mock(RedisTemplate.class); PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, reactionRepo, subRepo, notificationRepo, postReadService, - imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate); + imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, + pointHistoryRepository, PublishMode.DIRECT, redisTemplate); when(context.getBean(PostService.class)).thenReturn(service); when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L); @@ -162,6 +173,77 @@ class PostServiceTest { null, null, null, null, null, null, null, null, null)); } + @Test + void deletePostRemovesPointHistoriesAndRecalculatesPoints() { + PostRepository postRepo = mock(PostRepository.class); + UserRepository userRepo = mock(UserRepository.class); + CategoryRepository catRepo = mock(CategoryRepository.class); + TagRepository tagRepo = mock(TagRepository.class); + LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class); + PollPostRepository pollPostRepo = mock(PollPostRepository.class); + PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class); + NotificationService notifService = mock(NotificationService.class); + SubscriptionService subService = mock(SubscriptionService.class); + CommentService commentService = mock(CommentService.class); + CommentRepository commentRepo = mock(CommentRepository.class); + ReactionRepository reactionRepo = mock(ReactionRepository.class); + PostSubscriptionRepository subRepo = mock(PostSubscriptionRepository.class); + NotificationRepository notificationRepo = mock(NotificationRepository.class); + PostReadService postReadService = mock(PostReadService.class); + ImageUploader imageUploader = mock(ImageUploader.class); + TaskScheduler taskScheduler = mock(TaskScheduler.class); + EmailSender emailSender = mock(EmailSender.class); + ApplicationContext context = mock(ApplicationContext.class); + PointService pointService = mock(PointService.class); + PostChangeLogService postChangeLogService = mock(PostChangeLogService.class); + PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class); + RedisTemplate redisTemplate = mock(RedisTemplate.class); + + PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, + pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, + reactionRepo, subRepo, notificationRepo, postReadService, + imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, + pointHistoryRepository, PublishMode.DIRECT, redisTemplate); + when(context.getBean(PostService.class)).thenReturn(service); + + Post post = new Post(); + post.setId(10L); + User author = new User(); + author.setId(20L); + author.setRole(Role.USER); + post.setAuthor(author); + + User historyUser = new User(); + historyUser.setId(30L); + + PointHistory history = new PointHistory(); + history.setUser(historyUser); + history.setPost(post); + + when(postRepo.findById(10L)).thenReturn(Optional.of(post)); + when(userRepo.findByUsername("author")).thenReturn(Optional.of(author)); + when(commentRepo.findByPostAndParentIsNullOrderByCreatedAtAsc(post)).thenReturn(List.of()); + when(reactionRepo.findByPost(post)).thenReturn(List.of()); + when(subRepo.findByPost(post)).thenReturn(List.of()); + when(notificationRepo.findByPost(post)).thenReturn(List.of()); + when(pointHistoryRepository.findByPost(post)).thenReturn(List.of(history)); + when(pointService.recalculateUserPoints(historyUser)).thenReturn(0); + + service.deletePost(10L, "author"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(pointHistoryRepository).saveAll(captor.capture()); + List savedHistories = captor.getValue(); + assertEquals(1, savedHistories.size()); + PointHistory savedHistory = savedHistories.get(0); + assertNull(savedHistory.getPost()); + assertNotNull(savedHistory.getDeletedAt()); + assertTrue(savedHistory.getDeletedAt().isBefore(LocalDateTime.now().plusSeconds(1))); + + verify(pointService).recalculateUserPoints(historyUser); + verify(userRepo).saveAll(any()); + } + @Test void finalizeLotteryNotifiesAuthor() { PostRepository postRepo = mock(PostRepository.class);