From 1a21ba89354f94dcf5c0516104a04b25f6e3d95b Mon Sep 17 00:00:00 2001 From: sivdead <923396178@qq.com> Date: Fri, 12 Sep 2025 11:08:59 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(posts):=20=E4=BC=98=E5=8C=96=E5=B8=96?= =?UTF-8?q?=E5=AD=90=E8=AF=84=E8=AE=BA=E7=BB=9F=E8=AE=A1=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 Post 模型中添加 commentCount 和 lastReplyAt 字段 - 在 CommentService 中实现更新帖子评论统计的方法 - 在 PostMapper 中使用 Post 模型中的评论统计字段 - 新增数据库迁移脚本,添加评论统计字段和索引 - 更新相关测试用例 --- .../java/com/openisle/mapper/PostMapper.java | 4 +- .../main/java/com/openisle/model/Post.java | 6 ++ .../com/openisle/service/CommentService.java | 27 ++++++ .../V5__add_comment_stats_to_posts.sql | 19 ++++ .../controller/PostControllerTest.java | 22 +++++ .../service/PostCommentStatsTest.java | 90 +++++++++++++++++++ .../src/test/resources/application.properties | 12 +++ 7 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V5__add_comment_stats_to_posts.sql create mode 100644 backend/src/test/java/com/openisle/service/PostCommentStatsTest.java diff --git a/backend/src/main/java/com/openisle/mapper/PostMapper.java b/backend/src/main/java/com/openisle/mapper/PostMapper.java index bd4a04826..3b1730828 100644 --- a/backend/src/main/java/com/openisle/mapper/PostMapper.java +++ b/backend/src/main/java/com/openisle/mapper/PostMapper.java @@ -67,7 +67,7 @@ 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.setCommentCount(post.getCommentCount()); dto.setStatus(post.getStatus()); dto.setPinnedAt(post.getPinnedAt()); dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded()); @@ -82,7 +82,7 @@ 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()); + LocalDateTime last = post.getLastReplyAt(); dto.setLastReplyAt(last != null ? last : post.getCreatedAt()); dto.setReward(0); dto.setSubscribed(false); diff --git a/backend/src/main/java/com/openisle/model/Post.java b/backend/src/main/java/com/openisle/model/Post.java index 3dcbbab3c..7b725eaa4 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 1076aa5e2..a1e5f3f54 100644 --- a/backend/src/main/java/com/openisle/service/CommentService.java +++ b/backend/src/main/java/com/openisle/service/CommentService.java @@ -69,6 +69,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, @@ -118,6 +122,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(), @@ -263,9 +271,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()) { @@ -311,4 +323,19 @@ public class CommentService { int replies = commentRepository.findByParentOrderByCreatedAtAsc(comment).size(); return reactions + replies; } + + /** + * Update post comment statistics (comment count and last reply time) + */ + private void updatePostCommentStats(Post post) { + long commentCount = commentRepository.countByPostId(post.getId()); + LocalDateTime lastReplyAt = commentRepository.findLastCommentTime(post); + + post.setCommentCount(commentCount); + 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 From f67e2208943729bd4399a1270177ebcf44b3c0cd Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 17 Sep 2025 14:14:55 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E6=97=A7=E5=B8=96=E5=AD=90=E7=9A=84?= =?UTF-8?q?last=5Freply=5Fat=E4=B9=9F=E8=A6=81=E5=8F=8A=E6=97=B6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=EF=BC=88=E4=BB=85=E4=B8=80=E6=AC=A1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/openisle/mapper/PostMapper.java | 5 ++++- .../java/com/openisle/service/CommentService.java | 12 ++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/openisle/mapper/PostMapper.java b/backend/src/main/java/com/openisle/mapper/PostMapper.java index d60da6154..05dfd532a 100644 --- a/backend/src/main/java/com/openisle/mapper/PostMapper.java +++ b/backend/src/main/java/com/openisle/mapper/PostMapper.java @@ -83,7 +83,10 @@ public class PostMapper { dto.setParticipants(participants.stream().map(userMapper::toAuthorDto).collect(Collectors.toList())); LocalDateTime last = post.getLastReplyAt(); - dto.setLastReplyAt(last != null ? last : post.getCreatedAt()); + if (last == null) { + commentService.updatePostCommentStats(post); + } + dto.setLastReplyAt(post.getLastReplyAt()); dto.setReward(0); dto.setSubscribed(false); dto.setType(post.getType()); diff --git a/backend/src/main/java/com/openisle/service/CommentService.java b/backend/src/main/java/com/openisle/service/CommentService.java index 1c182ac2c..3f322791a 100644 --- a/backend/src/main/java/com/openisle/service/CommentService.java +++ b/backend/src/main/java/com/openisle/service/CommentService.java @@ -346,12 +346,16 @@ public class CommentService { /** * Update post comment statistics (comment count and last reply time) */ - private void updatePostCommentStats(Post post) { + public void updatePostCommentStats(Post post) { long commentCount = commentRepository.countByPostId(post.getId()); - LocalDateTime lastReplyAt = commentRepository.findLastCommentTime(post); - post.setCommentCount(commentCount); - post.setLastReplyAt(lastReplyAt); + + 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={}", From 3d0d0496b6af930b5d980ab50e2d5812c104f001 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 17 Sep 2025 14:16:49 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20comment=20count=20=E6=94=BE=E5=9C=A8?= =?UTF-8?q?last=5Freply=5Fat=E5=90=8E=E6=9B=B4=E6=96=B0=EF=BC=8C=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E6=95=B0=E6=8D=AE=E6=AD=A3=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/com/openisle/mapper/PostMapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/openisle/mapper/PostMapper.java b/backend/src/main/java/com/openisle/mapper/PostMapper.java index 05dfd532a..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(post.getCommentCount()); dto.setStatus(post.getStatus()); dto.setPinnedAt(post.getPinnedAt()); dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded()); @@ -86,6 +85,7 @@ public class PostMapper { if (last == null) { commentService.updatePostCommentStats(post); } + dto.setCommentCount(post.getCommentCount()); dto.setLastReplyAt(post.getLastReplyAt()); dto.setReward(0); dto.setSubscribed(false);