diff --git a/backend/src/main/java/com/openisle/controller/PostController.java b/backend/src/main/java/com/openisle/controller/PostController.java index 86e27b06c..6c211fa29 100644 --- a/backend/src/main/java/com/openisle/controller/PostController.java +++ b/backend/src/main/java/com/openisle/controller/PostController.java @@ -217,11 +217,7 @@ public class PostController { // userVisitService.recordVisit(auth.getName()); // } - return postService - .defaultListPosts(ids, tids, page, pageSize) - .stream() - .map(postMapper::toSummaryDto) - .collect(Collectors.toList()); + return postMapper.toListDtos(postService.defaultListPosts(ids, tids, page, pageSize)); } @GetMapping("/recent") @@ -269,11 +265,7 @@ public class PostController { // userVisitService.recordVisit(auth.getName()); // } - return postService - .listPostsByViews(ids, tids, page, pageSize) - .stream() - .map(postMapper::toSummaryDto) - .collect(Collectors.toList()); + return postMapper.toListDtos(postService.listPostsByViews(ids, tids, page, pageSize)); } @GetMapping("/latest-reply") @@ -305,8 +297,7 @@ public class PostController { // userVisitService.recordVisit(auth.getName()); // } - List posts = postService.listPostsByLatestReply(ids, tids, page, pageSize); - return posts.stream().map(postMapper::toSummaryDto).collect(Collectors.toList()); + return postMapper.toListDtos(postService.listPostsByLatestReply(ids, tids, page, pageSize)); } @GetMapping("/featured") @@ -333,10 +324,6 @@ public class PostController { // if (auth != null) { // userVisitService.recordVisit(auth.getName()); // } - return postService - .listFeaturedPosts(ids, tids, page, pageSize) - .stream() - .map(postMapper::toSummaryDto) - .collect(Collectors.toList()); + return postMapper.toListDtos(postService.listFeaturedPosts(ids, tids, page, pageSize)); } } diff --git a/backend/src/main/java/com/openisle/mapper/PostMapper.java b/backend/src/main/java/com/openisle/mapper/PostMapper.java index 876b27863..24966f000 100644 --- a/backend/src/main/java/com/openisle/mapper/PostMapper.java +++ b/backend/src/main/java/com/openisle/mapper/PostMapper.java @@ -48,6 +48,38 @@ public class PostMapper { return dto; } + public List toListDtos(List posts) { + if (posts == null || posts.isEmpty()) { + return List.of(); + } + Map> participantsMap = commentService.getParticipantsForPosts(posts, 5); + return posts + .stream() + .map(post -> { + PostSummaryDto dto = new PostSummaryDto(); + applyListFields(post, dto); + List participants = participantsMap.get(post.getId()); + if (participants != null) { + dto.setParticipants( + participants.stream().map(userMapper::toAuthorDto).collect(Collectors.toList()) + ); + } else { + dto.setParticipants(List.of()); + } + dto.setReactions(List.of()); + return dto; + }) + .collect(Collectors.toList()); + } + + public PostSummaryDto toListDto(Post post) { + PostSummaryDto dto = new PostSummaryDto(); + applyListFields(post, dto); + dto.setParticipants(List.of()); + dto.setReactions(List.of()); + return dto; + } + public PostDetailDto toDetailDto(Post post, String viewer) { PostDetailDto dto = new PostDetailDto(); applyCommon(post, dto); @@ -61,6 +93,25 @@ public class PostMapper { return dto; } + private void applyListFields(Post post, PostSummaryDto dto) { + dto.setId(post.getId()); + dto.setTitle(post.getTitle()); + dto.setContent(post.getContent()); + dto.setCreatedAt(post.getCreatedAt()); + dto.setAuthor(userMapper.toAuthorDto(post.getAuthor())); + 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.setLastReplyAt(post.getLastReplyAt()); + dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded()); + dto.setClosed(post.isClosed()); + dto.setVisibleScope(post.getVisibleScope()); + dto.setType(post.getType()); + } + private void applyCommon(Post post, PostSummaryDto dto) { dto.setId(post.getId()); dto.setTitle(post.getTitle()); diff --git a/backend/src/main/java/com/openisle/repository/CommentRepository.java b/backend/src/main/java/com/openisle/repository/CommentRepository.java index 3a0d2fe50..ef7c56873 100644 --- a/backend/src/main/java/com/openisle/repository/CommentRepository.java +++ b/backend/src/main/java/com/openisle/repository/CommentRepository.java @@ -25,6 +25,13 @@ public interface CommentRepository extends JpaRepository { @org.springframework.data.repository.query.Param("post") Post post ); + @org.springframework.data.jpa.repository.Query( + "SELECT DISTINCT c.post.id, c.author FROM Comment c WHERE c.post.id IN :postIds" + ) + java.util.List findDistinctAuthorsByPostIds( + @org.springframework.data.repository.query.Param("postIds") java.util.List postIds + ); + @org.springframework.data.jpa.repository.Query( "SELECT MAX(c.createdAt) FROM Comment c WHERE c.post = :post" ) diff --git a/backend/src/main/java/com/openisle/repository/PostRepository.java b/backend/src/main/java/com/openisle/repository/PostRepository.java index 107c692de..098787b3b 100644 --- a/backend/src/main/java/com/openisle/repository/PostRepository.java +++ b/backend/src/main/java/com/openisle/repository/PostRepository.java @@ -19,6 +19,8 @@ public interface PostRepository extends JpaRepository { List findByStatusOrderByCreatedAtDesc(PostStatus status, Pageable pageable); List findByStatusOrderByViewsDesc(PostStatus status); List findByStatusOrderByViewsDesc(PostStatus status, Pageable pageable); + List findByStatusOrderByPinnedAtDescViewsDesc(PostStatus status, Pageable pageable); + List findByStatusOrderByPinnedAtDescLastReplyAtDesc(PostStatus status, Pageable pageable); List findByStatusAndCreatedAtGreaterThanEqualOrderByCreatedAtDesc( PostStatus status, LocalDateTime createdAt @@ -43,6 +45,16 @@ public interface PostRepository extends JpaRepository { PostStatus status, Pageable pageable ); + List findByCategoryInAndStatusOrderByPinnedAtDescViewsDesc( + List categories, + PostStatus status, + Pageable pageable + ); + List findByCategoryInAndStatusOrderByPinnedAtDescLastReplyAtDesc( + List categories, + PostStatus status, + Pageable pageable + ); List findDistinctByTagsInAndStatus(List tags, PostStatus status); List findDistinctByTagsInAndStatus(List tags, PostStatus status, Pageable pageable); List findDistinctByTagsInAndStatusOrderByCreatedAtDesc(List tags, PostStatus status); @@ -132,6 +144,26 @@ public interface PostRepository extends JpaRepository { Pageable pageable ); + @Query( + "SELECT p FROM Post p JOIN p.tags t WHERE t IN :tags AND p.status = :status GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :tagCount ORDER BY p.pinnedAt DESC, p.views DESC" + ) + List findByAllTagsOrderByPinnedAtDescViewsDesc( + @Param("tags") List tags, + @Param("status") PostStatus status, + @Param("tagCount") long tagCount, + Pageable pageable + ); + + @Query( + "SELECT p FROM Post p JOIN p.tags t WHERE t IN :tags AND p.status = :status GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :tagCount ORDER BY p.pinnedAt DESC, p.lastReplyAt DESC" + ) + List findByAllTagsOrderByPinnedAtDescLastReplyAtDesc( + @Param("tags") List tags, + @Param("status") PostStatus status, + @Param("tagCount") long tagCount, + Pageable pageable + ); + @Query( "SELECT p FROM Post p JOIN p.tags t WHERE p.category IN :categories AND t IN :tags AND p.status = :status GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :tagCount" ) @@ -174,6 +206,28 @@ public interface PostRepository extends JpaRepository { Pageable pageable ); + @Query( + "SELECT p FROM Post p JOIN p.tags t WHERE p.category IN :categories AND t IN :tags AND p.status = :status GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :tagCount ORDER BY p.pinnedAt DESC, p.views DESC" + ) + List findByCategoriesAndAllTagsOrderByPinnedAtDescViewsDesc( + @Param("categories") List categories, + @Param("tags") List tags, + @Param("status") PostStatus status, + @Param("tagCount") long tagCount, + Pageable pageable + ); + + @Query( + "SELECT p FROM Post p JOIN p.tags t WHERE p.category IN :categories AND t IN :tags AND p.status = :status GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :tagCount ORDER BY p.pinnedAt DESC, p.lastReplyAt DESC" + ) + List findByCategoriesAndAllTagsOrderByPinnedAtDescLastReplyAtDesc( + @Param("categories") List categories, + @Param("tags") List tags, + @Param("status") PostStatus status, + @Param("tagCount") long tagCount, + Pageable pageable + ); + @Query( "SELECT p FROM Post p JOIN p.tags t WHERE p.category IN :categories AND t IN :tags AND p.status = :status GROUP BY p.id HAVING COUNT(DISTINCT t.id) = :tagCount ORDER BY p.createdAt DESC" ) diff --git a/backend/src/main/java/com/openisle/service/CommentService.java b/backend/src/main/java/com/openisle/service/CommentService.java index 908a5f30d..09c3c0e47 100644 --- a/backend/src/main/java/com/openisle/service/CommentService.java +++ b/backend/src/main/java/com/openisle/service/CommentService.java @@ -21,8 +21,12 @@ import com.openisle.service.NotificationService; import com.openisle.service.PointService; import com.openisle.service.SubscriptionService; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -316,6 +320,37 @@ public class CommentService { return result; } + public Map> getParticipantsForPosts(List posts, int limit) { + if (posts == null || posts.isEmpty()) { + return Map.of(); + } + Map> map = new HashMap<>(); + List postIds = new ArrayList<>(posts.size()); + for (Post post : posts) { + postIds.add(post.getId()); + LinkedHashSet set = new LinkedHashSet<>(); + set.add(post.getAuthor()); + map.put(post.getId(), set); + } + for (Object[] row : commentRepository.findDistinctAuthorsByPostIds(postIds)) { + Long postId = (Long) row[0]; + User author = (User) row[1]; + LinkedHashSet set = map.get(postId); + if (set != null) { + set.add(author); + } + } + Map> result = new HashMap<>(map.size()); + for (Map.Entry> entry : map.entrySet()) { + List list = new ArrayList<>(entry.getValue()); + if (list.size() > limit) { + list = list.subList(0, limit); + } + result.put(entry.getKey(), list); + } + return result; + } + public java.util.List getCommentsByIds(java.util.List ids) { log.debug("getCommentsByIds called for ids {}", ids); java.util.List comments = commentRepository.findAllById(ids); diff --git a/backend/src/main/java/com/openisle/service/PostService.java b/backend/src/main/java/com/openisle/service/PostService.java index a55dcc4da..acea0b77d 100644 --- a/backend/src/main/java/com/openisle/service/PostService.java +++ b/backend/src/main/java/com/openisle/service/PostService.java @@ -339,6 +339,7 @@ public class PostService { post.setCategory(category); post.setTags(new HashSet<>(tags)); post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED); + post.setLastReplyAt(LocalDateTime.now()); // 什么都没设置的情况下,默认为ALL if (Objects.isNull(postVisibleScopeType)) { @@ -809,9 +810,10 @@ public class PostService { boolean hasTags = tagIds != null && !tagIds.isEmpty(); java.util.List posts; + Pageable pageable = buildPageable(page, pageSize); if (!hasCategories && !hasTags) { - posts = postRepository.findByStatusOrderByViewsDesc(PostStatus.PUBLISHED); + posts = postRepository.findByStatusOrderByPinnedAtDescViewsDesc(PostStatus.PUBLISHED, pageable); } else if (hasCategories) { java.util.List categories = categoryRepository.findAllById(categoryIds); if (categories.isEmpty()) { @@ -822,16 +824,18 @@ public class PostService { if (tags.isEmpty()) { return java.util.List.of(); } - posts = postRepository.findByCategoriesAndAllTagsOrderByViewsDesc( + posts = postRepository.findByCategoriesAndAllTagsOrderByPinnedAtDescViewsDesc( categories, tags, PostStatus.PUBLISHED, - tags.size() + tags.size(), + pageable ); } else { - posts = postRepository.findByCategoryInAndStatusOrderByViewsDesc( + posts = postRepository.findByCategoryInAndStatusOrderByPinnedAtDescViewsDesc( categories, - PostStatus.PUBLISHED + PostStatus.PUBLISHED, + pageable ); } } else { @@ -839,10 +843,15 @@ public class PostService { if (tags.isEmpty()) { return java.util.List.of(); } - posts = postRepository.findByAllTagsOrderByViewsDesc(tags, PostStatus.PUBLISHED, tags.size()); + posts = postRepository.findByAllTagsOrderByPinnedAtDescViewsDesc( + tags, + PostStatus.PUBLISHED, + tags.size(), + pageable + ); } - return paginate(sortByPinnedAndViews(posts), page, pageSize); + return posts; } public List listPostsByLatestReply(Integer page, Integer pageSize) { @@ -859,9 +868,13 @@ public class PostService { boolean hasTags = tagIds != null && !tagIds.isEmpty(); java.util.List posts; + Pageable pageable = buildPageable(page, pageSize); if (!hasCategories && !hasTags) { - posts = postRepository.findByStatusOrderByCreatedAtDesc(PostStatus.PUBLISHED); + posts = postRepository.findByStatusOrderByPinnedAtDescLastReplyAtDesc( + PostStatus.PUBLISHED, + pageable + ); } else if (hasCategories) { java.util.List categories = categoryRepository.findAllById(categoryIds); if (categories.isEmpty()) { @@ -872,16 +885,18 @@ public class PostService { if (tags.isEmpty()) { return java.util.List.of(); } - posts = postRepository.findByCategoriesAndAllTagsOrderByCreatedAtDesc( + posts = postRepository.findByCategoriesAndAllTagsOrderByPinnedAtDescLastReplyAtDesc( categories, tags, PostStatus.PUBLISHED, - tags.size() + tags.size(), + pageable ); } else { - posts = postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc( + posts = postRepository.findByCategoryInAndStatusOrderByPinnedAtDescLastReplyAtDesc( categories, - PostStatus.PUBLISHED + PostStatus.PUBLISHED, + pageable ); } } else { @@ -889,14 +904,15 @@ public class PostService { if (tags.isEmpty()) { return new ArrayList<>(); } - posts = postRepository.findByAllTagsOrderByCreatedAtDesc( + posts = postRepository.findByAllTagsOrderByPinnedAtDescLastReplyAtDesc( tags, PostStatus.PUBLISHED, - tags.size() + tags.size(), + pageable ); } - return paginate(sortByPinnedAndLastReply(posts), page, pageSize); + return posts; } public List listPostsByCategories( @@ -1394,6 +1410,13 @@ public class PostService { .toList(); } + private Pageable buildPageable(Integer page, Integer pageSize) { + if (page == null || pageSize == null) { + return Pageable.unpaged(); + } + return PageRequest.of(page, pageSize); + } + private List paginate(List posts, Integer page, Integer pageSize) { if (page == null || pageSize == null) { return posts; diff --git a/backend/src/main/resources/db/migration/V10__backfill_post_last_reply_at.sql b/backend/src/main/resources/db/migration/V10__backfill_post_last_reply_at.sql new file mode 100644 index 000000000..ade69638d --- /dev/null +++ b/backend/src/main/resources/db/migration/V10__backfill_post_last_reply_at.sql @@ -0,0 +1,4 @@ +-- Backfill last_reply_at for posts without comments to preserve latest-reply ordering +UPDATE posts +SET last_reply_at = created_at +WHERE last_reply_at IS NULL;