diff --git a/backend/src/main/java/com/openisle/mapper/PostMapper.java b/backend/src/main/java/com/openisle/mapper/PostMapper.java index 959129236..a350908c4 100644 --- a/backend/src/main/java/com/openisle/mapper/PostMapper.java +++ b/backend/src/main/java/com/openisle/mapper/PostMapper.java @@ -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 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()); diff --git a/backend/src/main/java/com/openisle/model/Post.java b/backend/src/main/java/com/openisle/model/Post.java index 7e5df2a4f..e52a50a1b 100644 --- a/backend/src/main/java/com/openisle/model/Post.java +++ b/backend/src/main/java/com/openisle/model/Post.java @@ -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; } diff --git a/backend/src/main/java/com/openisle/service/CommentService.java b/backend/src/main/java/com/openisle/service/CommentService.java index cf09f2a19..3f322791a 100644 --- a/backend/src/main/java/com/openisle/service/CommentService.java +++ b/backend/src/main/java/com/openisle/service/CommentService.java @@ -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); + } } diff --git a/backend/src/main/resources/db/migration/V5__add_comment_stats_to_posts.sql b/backend/src/main/resources/db/migration/V5__add_comment_stats_to_posts.sql new file mode 100644 index 000000000..0e375e8bb --- /dev/null +++ b/backend/src/main/resources/db/migration/V5__add_comment_stats_to_posts.sql @@ -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 + ); diff --git a/backend/src/test/java/com/openisle/controller/PostControllerTest.java b/backend/src/test/java/com/openisle/controller/PostControllerTest.java index 5b667e926..f755424df 100644 --- a/backend/src/test/java/com/openisle/controller/PostControllerTest.java +++ b/backend/src/test/java/com/openisle/controller/PostControllerTest.java @@ -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"); diff --git a/backend/src/test/java/com/openisle/service/PostCommentStatsTest.java b/backend/src/test/java/com/openisle/service/PostCommentStatsTest.java new file mode 100644 index 000000000..124c0d280 --- /dev/null +++ b/backend/src/test/java/com/openisle/service/PostCommentStatsTest.java @@ -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()); + } +} diff --git a/backend/src/test/resources/application.properties b/backend/src/test/resources/application.properties index b132b8974..b65fb13f2 100644 --- a/backend/src/test/resources/application.properties +++ b/backend/src/test/resources/application.properties @@ -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