mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-06 23:21:16 +08:00
Merge pull request #979 from sivdead/optimize-post-list-n+1
主页列表接口优化,优化帖子评论统计性能
This commit is contained in:
@@ -67,7 +67,6 @@ public class PostMapper {
|
||||
dto.setCategory(categoryMapper.toDto(post.getCategory()));
|
||||
dto.setTags(post.getTags().stream().map(tagMapper::toDto).collect(Collectors.toList()));
|
||||
dto.setViews(post.getViews());
|
||||
dto.setCommentCount(commentService.countComments(post.getId()));
|
||||
dto.setStatus(post.getStatus());
|
||||
dto.setPinnedAt(post.getPinnedAt());
|
||||
dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded());
|
||||
@@ -82,8 +81,12 @@ public class PostMapper {
|
||||
List<User> participants = commentService.getParticipants(post.getId(), 5);
|
||||
dto.setParticipants(participants.stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
|
||||
|
||||
LocalDateTime last = commentService.getLastCommentTime(post.getId());
|
||||
dto.setLastReplyAt(last != null ? last : post.getCreatedAt());
|
||||
LocalDateTime last = post.getLastReplyAt();
|
||||
if (last == null) {
|
||||
commentService.updatePostCommentStats(post);
|
||||
}
|
||||
dto.setCommentCount(post.getCommentCount());
|
||||
dto.setLastReplyAt(post.getLastReplyAt());
|
||||
dto.setReward(0);
|
||||
dto.setSubscribed(false);
|
||||
dto.setType(post.getType());
|
||||
|
||||
@@ -72,4 +72,10 @@ public class Post {
|
||||
|
||||
@Column(nullable = true)
|
||||
private Boolean rssExcluded = true;
|
||||
|
||||
@Column(nullable = false)
|
||||
private long commentCount = 0;
|
||||
|
||||
@Column(nullable = true)
|
||||
private LocalDateTime lastReplyAt;
|
||||
}
|
||||
|
||||
@@ -76,6 +76,10 @@ public class CommentService {
|
||||
comment.setContent(content);
|
||||
comment = commentRepository.save(comment);
|
||||
log.debug("Comment {} saved for post {}", comment.getId(), postId);
|
||||
|
||||
// Update post comment statistics
|
||||
updatePostCommentStats(post);
|
||||
|
||||
imageUploader.addReferences(imageUploader.extractUrls(content));
|
||||
if (!author.getId().equals(post.getAuthor().getId())) {
|
||||
notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment,
|
||||
@@ -129,6 +133,10 @@ public class CommentService {
|
||||
comment.setContent(content);
|
||||
comment = commentRepository.save(comment);
|
||||
log.debug("Reply {} saved for parent {}", comment.getId(), parentId);
|
||||
|
||||
// Update post comment statistics
|
||||
updatePostCommentStats(parent.getPost());
|
||||
|
||||
imageUploader.addReferences(imageUploader.extractUrls(content));
|
||||
if (!author.getId().equals(parent.getAuthor().getId())) {
|
||||
notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(),
|
||||
@@ -282,9 +290,13 @@ public class CommentService {
|
||||
imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent()));
|
||||
|
||||
// 逻辑删除评论
|
||||
Post post = comment.getPost();
|
||||
commentRepository.delete(comment);
|
||||
// 删除积分历史
|
||||
pointHistoryRepository.deleteAll(pointHistories);
|
||||
|
||||
// Update post comment statistics
|
||||
updatePostCommentStats(post);
|
||||
|
||||
// 重新计算受影响用户的积分
|
||||
if (!usersToRecalculate.isEmpty()) {
|
||||
@@ -330,4 +342,23 @@ public class CommentService {
|
||||
int replies = commentRepository.findByParentOrderByCreatedAtAsc(comment).size();
|
||||
return reactions + replies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update post comment statistics (comment count and last reply time)
|
||||
*/
|
||||
public void updatePostCommentStats(Post post) {
|
||||
long commentCount = commentRepository.countByPostId(post.getId());
|
||||
post.setCommentCount(commentCount);
|
||||
|
||||
LocalDateTime lastReplyAt = commentRepository.findLastCommentTime(post);
|
||||
if (lastReplyAt == null) {
|
||||
post.setLastReplyAt(post.getCreatedAt());
|
||||
} else {
|
||||
post.setLastReplyAt(lastReplyAt);
|
||||
}
|
||||
postRepository.save(post);
|
||||
|
||||
log.debug("Updated post {} stats: commentCount={}, lastReplyAt={}",
|
||||
post.getId(), commentCount, lastReplyAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Add comment count and last reply time fields to posts table for performance optimization
|
||||
ALTER TABLE posts ADD COLUMN comment_count BIGINT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE posts ADD COLUMN last_reply_at DATETIME(6) NULL;
|
||||
|
||||
-- Add index on last_reply_at for sorting by latest reply
|
||||
CREATE INDEX idx_posts_last_reply_at ON posts(last_reply_at);
|
||||
|
||||
-- Initialize comment_count and last_reply_at with existing data
|
||||
UPDATE posts p SET
|
||||
comment_count = (
|
||||
SELECT COUNT(*)
|
||||
FROM comments c
|
||||
WHERE c.post_id = p.id AND c.deleted_at IS NULL
|
||||
),
|
||||
last_reply_at = (
|
||||
SELECT MAX(c.created_at)
|
||||
FROM comments c
|
||||
WHERE c.post_id = p.id AND c.deleted_at IS NULL
|
||||
);
|
||||
@@ -55,6 +55,10 @@ class PostControllerTest {
|
||||
private UserVisitService userVisitService;
|
||||
@MockBean
|
||||
private PostReadService postReadService;
|
||||
@MockBean
|
||||
private MedalService medalService;
|
||||
@MockBean
|
||||
private com.openisle.repository.PollVoteRepository pollVoteRepository;
|
||||
|
||||
@Test
|
||||
void createAndGetPost() throws Exception {
|
||||
@@ -63,9 +67,13 @@ class PostControllerTest {
|
||||
Category cat = new Category();
|
||||
cat.setId(1L);
|
||||
cat.setName("tech");
|
||||
cat.setDescription("Technology category");
|
||||
cat.setIcon("tech-icon");
|
||||
Tag tag = new Tag();
|
||||
tag.setId(1L);
|
||||
tag.setName("java");
|
||||
tag.setDescription("Java programming language");
|
||||
tag.setIcon("java-icon");
|
||||
Post post = new Post();
|
||||
post.setId(1L);
|
||||
post.setTitle("t");
|
||||
@@ -111,9 +119,13 @@ class PostControllerTest {
|
||||
Category cat = new Category();
|
||||
cat.setId(1L);
|
||||
cat.setName("tech");
|
||||
cat.setDescription("Technology category");
|
||||
cat.setIcon("tech-icon");
|
||||
Tag tag = new Tag();
|
||||
tag.setId(1L);
|
||||
tag.setName("java");
|
||||
tag.setDescription("Java programming language");
|
||||
tag.setIcon("java-icon");
|
||||
Post post = new Post();
|
||||
post.setId(1L);
|
||||
post.setTitle("t2");
|
||||
@@ -147,9 +159,13 @@ class PostControllerTest {
|
||||
Category cat = new Category();
|
||||
cat.setId(1L);
|
||||
cat.setName("tech");
|
||||
cat.setDescription("Technology category");
|
||||
cat.setIcon("tech-icon");
|
||||
Tag tag = new Tag();
|
||||
tag.setId(1L);
|
||||
tag.setName("java");
|
||||
tag.setDescription("Java programming language");
|
||||
tag.setIcon("java-icon");
|
||||
Post post = new Post();
|
||||
post.setId(2L);
|
||||
post.setTitle("hello");
|
||||
@@ -197,9 +213,13 @@ class PostControllerTest {
|
||||
Category cat = new Category();
|
||||
cat.setId(1L);
|
||||
cat.setName("tech");
|
||||
cat.setDescription("Technology category");
|
||||
cat.setIcon("tech-icon");
|
||||
Tag tag = new Tag();
|
||||
tag.setId(1L);
|
||||
tag.setName("java");
|
||||
tag.setDescription("Java programming language");
|
||||
tag.setIcon("java-icon");
|
||||
Post post = new Post();
|
||||
post.setId(1L);
|
||||
post.setTitle("t");
|
||||
@@ -262,6 +282,8 @@ class PostControllerTest {
|
||||
Category cat = new Category();
|
||||
cat.setId(1L);
|
||||
cat.setName("tech");
|
||||
cat.setDescription("Technology category");
|
||||
cat.setIcon("tech-icon");
|
||||
Post post = new Post();
|
||||
post.setId(1L);
|
||||
post.setTitle("t");
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.*;
|
||||
import com.openisle.repository.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest
|
||||
@TestPropertySource(locations = "classpath:application.properties")
|
||||
@Transactional
|
||||
public class PostCommentStatsTest {
|
||||
|
||||
@Autowired
|
||||
private PostRepository postRepository;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private CategoryRepository categoryRepository;
|
||||
|
||||
@Autowired
|
||||
private TagRepository tagRepository;
|
||||
|
||||
@Autowired
|
||||
private CommentService commentService;
|
||||
|
||||
@Test
|
||||
public void testPostCommentStatsUpdate() {
|
||||
// Create test user
|
||||
User user = new User();
|
||||
user.setUsername("testuser");
|
||||
user.setEmail("test@example.com");
|
||||
user.setPassword("hash");
|
||||
user = userRepository.save(user);
|
||||
|
||||
// Create test category
|
||||
Category category = new Category();
|
||||
category.setName("Test Category");
|
||||
category.setDescription("Test Category Description");
|
||||
category.setIcon("test-icon");
|
||||
category = categoryRepository.save(category);
|
||||
|
||||
// Create test tag
|
||||
Tag tag = new Tag();
|
||||
tag.setName("Test Tag");
|
||||
tag.setDescription("Test Tag Description");
|
||||
tag.setIcon("test-tag-icon");
|
||||
tag = tagRepository.save(tag);
|
||||
|
||||
// Create test post
|
||||
Post post = new Post();
|
||||
post.setTitle("Test Post");
|
||||
post.setContent("Test content");
|
||||
post.setAuthor(user);
|
||||
post.setCategory(category);
|
||||
post.getTags().add(tag);
|
||||
post.setStatus(PostStatus.PUBLISHED);
|
||||
post.setCommentCount(0L);
|
||||
post = postRepository.save(post);
|
||||
|
||||
// Verify initial state
|
||||
assertEquals(0L, post.getCommentCount());
|
||||
assertNull(post.getLastReplyAt());
|
||||
|
||||
// Add a comment
|
||||
commentService.addComment("testuser", post.getId(), "Test comment");
|
||||
|
||||
// Refresh post from database
|
||||
post = postRepository.findById(post.getId()).orElseThrow();
|
||||
|
||||
// Verify comment count and last reply time are updated
|
||||
assertEquals(1L, post.getCommentCount());
|
||||
assertNotNull(post.getLastReplyAt());
|
||||
|
||||
// Add another comment
|
||||
commentService.addComment("testuser", post.getId(), "Another comment");
|
||||
|
||||
// Refresh post again
|
||||
post = postRepository.findById(post.getId()).orElseThrow();
|
||||
|
||||
// Verify comment count is updated
|
||||
assertEquals(2L, post.getCommentCount());
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,18 @@ spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
spring.jpa.hibernate.ddl-auto=create-drop
|
||||
|
||||
springdoc.info.title=openisle
|
||||
springdoc.info.description=Test API documentation
|
||||
springdoc.info.version=1.0.0
|
||||
springdoc.info.scheme=Bearer
|
||||
springdoc.info.header=Authorization
|
||||
|
||||
|
||||
rabbitmq.queue.durable=true
|
||||
rabbitmq.sharding.enabled=true
|
||||
|
||||
resend.api.key=dummy
|
||||
resend.from.email=dummy@example.com
|
||||
cos.base-url=http://test.example.com
|
||||
cos.secret-id=dummy
|
||||
cos.secret-key=dummy
|
||||
@@ -18,6 +29,7 @@ app.upload.max-size=1048576
|
||||
app.jwt.secret=TestSecret
|
||||
app.jwt.reason-secret=TestReasonSecret
|
||||
app.jwt.reset-secret=TestResetSecret
|
||||
app.jwt.invite-secret=TestInviteSecret
|
||||
app.jwt.expiration=3600000
|
||||
|
||||
# Default publish mode for tests
|
||||
|
||||
Reference in New Issue
Block a user