Handle point history cleanup when deleting posts

This commit is contained in:
Tim
2025-09-17 12:29:09 +08:00
parent b28e8d4bc9
commit 28e3ebb911
3 changed files with 115 additions and 10 deletions

View File

@@ -1,8 +1,9 @@
package com.openisle.repository; package com.openisle.repository;
import com.openisle.model.PointHistory;
import com.openisle.model.User;
import com.openisle.model.Comment; 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 org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -14,6 +15,8 @@ public interface PointHistoryRepository extends JpaRepository<PointHistory, Long
long countByUser(User user); long countByUser(User user);
List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt); List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt);
List<PointHistory> findByComment(Comment comment); List<PointHistory> findByComment(Comment comment);
List<PointHistory> findByPost(Post post);
} }

View File

@@ -9,14 +9,12 @@ import com.openisle.repository.PollPostRepository;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.repository.CategoryRepository; import com.openisle.repository.CategoryRepository;
import com.openisle.repository.TagRepository; 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.CommentRepository;
import com.openisle.repository.ReactionRepository; import com.openisle.repository.ReactionRepository;
import com.openisle.repository.PostSubscriptionRepository; import com.openisle.repository.PostSubscriptionRepository;
import com.openisle.repository.NotificationRepository; import com.openisle.repository.NotificationRepository;
import com.openisle.repository.PollVoteRepository; import com.openisle.repository.PollVoteRepository;
import com.openisle.repository.PointHistoryRepository;
import com.openisle.exception.RateLimitException; import com.openisle.exception.RateLimitException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -74,6 +72,7 @@ public class PostService {
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final PointService pointService; private final PointService pointService;
private final PostChangeLogService postChangeLogService; private final PostChangeLogService postChangeLogService;
private final PointHistoryRepository pointHistoryRepository;
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>(); private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
@Value("${app.website-url:https://www.open-isle.com}") @Value("${app.website-url:https://www.open-isle.com}")
private String websiteUrl; private String websiteUrl;
@@ -102,6 +101,7 @@ public class PostService {
ApplicationContext applicationContext, ApplicationContext applicationContext,
PointService pointService, PointService pointService,
PostChangeLogService postChangeLogService, PostChangeLogService postChangeLogService,
PointHistoryRepository pointHistoryRepository,
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode, @Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode,
RedisTemplate redisTemplate) { RedisTemplate redisTemplate) {
this.postRepository = postRepository; this.postRepository = postRepository;
@@ -125,6 +125,7 @@ public class PostService {
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
this.pointService = pointService; this.pointService = pointService;
this.postChangeLogService = postChangeLogService; this.postChangeLogService = postChangeLogService;
this.pointHistoryRepository = pointHistoryRepository;
this.publishMode = publishMode; this.publishMode = publishMode;
this.redisTemplate = redisTemplate; this.redisTemplate = redisTemplate;
@@ -861,6 +862,25 @@ public class PostService {
notificationRepository.deleteAll(notificationRepository.findByPost(post)); notificationRepository.deleteAll(notificationRepository.findByPost(post));
postReadService.deleteByPost(post); postReadService.deleteByPost(post);
imageUploader.removeReferences(imageUploader.extractUrls(post.getContent())); imageUploader.removeReferences(imageUploader.extractUrls(post.getContent()));
List<PointHistory> pointHistories = pointHistoryRepository.findByPost(post);
Set<User> 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) { if (post instanceof LotteryPost lp) {
ScheduledFuture<?> future = scheduledFinalizations.remove(lp.getId()); ScheduledFuture<?> future = scheduledFinalizations.remove(lp.getId());
if (future != null) { if (future != null) {

View File

@@ -10,8 +10,11 @@ import org.springframework.data.redis.core.RedisTemplate;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import java.util.Optional; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.mockito.ArgumentCaptor;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@@ -39,12 +42,14 @@ class PostServiceTest {
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class); PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class); RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, 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(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post(); Post post = new Post();
@@ -60,6 +65,7 @@ class PostServiceTest {
when(reactionRepo.findByPost(post)).thenReturn(List.of()); when(reactionRepo.findByPost(post)).thenReturn(List.of());
when(subRepo.findByPost(post)).thenReturn(List.of()); when(subRepo.findByPost(post)).thenReturn(List.of());
when(notificationRepo.findByPost(post)).thenReturn(List.of()); when(notificationRepo.findByPost(post)).thenReturn(List.of());
when(pointHistoryRepository.findByPost(post)).thenReturn(List.of());
service.deletePost(1L, "alice"); service.deletePost(1L, "alice");
@@ -90,12 +96,14 @@ class PostServiceTest {
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class); PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class); RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, 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(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post(); Post post = new Post();
@@ -117,6 +125,7 @@ class PostServiceTest {
when(reactionRepo.findByPost(post)).thenReturn(List.of()); when(reactionRepo.findByPost(post)).thenReturn(List.of());
when(subRepo.findByPost(post)).thenReturn(List.of()); when(subRepo.findByPost(post)).thenReturn(List.of());
when(notificationRepo.findByPost(post)).thenReturn(List.of()); when(notificationRepo.findByPost(post)).thenReturn(List.of());
when(pointHistoryRepository.findByPost(post)).thenReturn(List.of());
service.deletePost(1L, "admin"); service.deletePost(1L, "admin");
@@ -147,12 +156,14 @@ class PostServiceTest {
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class); PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PointHistoryRepository pointHistoryRepository = mock(PointHistoryRepository.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class); RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, 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(context.getBean(PostService.class)).thenReturn(service);
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L); when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
@@ -162,6 +173,77 @@ class PostServiceTest {
null, null, null, null, null, null, null, null, null)); 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<List<PointHistory>> captor = ArgumentCaptor.forClass(List.class);
verify(pointHistoryRepository).saveAll(captor.capture());
List<PointHistory> 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 @Test
void finalizeLotteryNotifiesAuthor() { void finalizeLotteryNotifiesAuthor() {
PostRepository postRepo = mock(PostRepository.class); PostRepository postRepo = mock(PostRepository.class);