Compare commits

...

5 Commits

Author SHA1 Message Date
Tim
e5b386cdc2 Merge pull request #1136 from nagisa77/bugfix/1053
fix: 性能优化,首页下拉更新,实测6秒左右,稍慢 #1053
2026-01-16 16:47:46 +08:00
Tim
179699dd66 Merge pull request #1135 from nagisa77/bugfix/1130
fix: 1. 修复dev_local_backend报错问题 2.由于内存占用过高,直接去除opensearch
2026-01-16 16:45:35 +08:00
Tim
ef39b5fedf fix: 性能优化,首页下拉更新,实测6秒左右,稍慢 #1053 2026-01-16 16:45:07 +08:00
Tim
e13ee1ca46 fix: 1. 修复dev_local_backend报错问题 2.由于内存占用过高,直接去除opensearch 2026-01-16 15:22:59 +08:00
Tim
09f1435e33 Merge pull request #1134 from nagisa77/feature/view_history
fix: add view history logic
2026-01-16 15:13:22 +08:00
8 changed files with 248 additions and 88 deletions

View File

@@ -217,11 +217,7 @@ public class PostController {
// userVisitService.recordVisit(auth.getName()); // userVisitService.recordVisit(auth.getName());
// } // }
return postService return postMapper.toListDtos(postService.defaultListPosts(ids, tids, page, pageSize));
.defaultListPosts(ids, tids, page, pageSize)
.stream()
.map(postMapper::toSummaryDto)
.collect(Collectors.toList());
} }
@GetMapping("/recent") @GetMapping("/recent")
@@ -269,11 +265,7 @@ public class PostController {
// userVisitService.recordVisit(auth.getName()); // userVisitService.recordVisit(auth.getName());
// } // }
return postService return postMapper.toListDtos(postService.listPostsByViews(ids, tids, page, pageSize));
.listPostsByViews(ids, tids, page, pageSize)
.stream()
.map(postMapper::toSummaryDto)
.collect(Collectors.toList());
} }
@GetMapping("/latest-reply") @GetMapping("/latest-reply")
@@ -305,8 +297,7 @@ public class PostController {
// userVisitService.recordVisit(auth.getName()); // userVisitService.recordVisit(auth.getName());
// } // }
List<Post> posts = postService.listPostsByLatestReply(ids, tids, page, pageSize); return postMapper.toListDtos(postService.listPostsByLatestReply(ids, tids, page, pageSize));
return posts.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
} }
@GetMapping("/featured") @GetMapping("/featured")
@@ -333,10 +324,6 @@ public class PostController {
// if (auth != null) { // if (auth != null) {
// userVisitService.recordVisit(auth.getName()); // userVisitService.recordVisit(auth.getName());
// } // }
return postService return postMapper.toListDtos(postService.listFeaturedPosts(ids, tids, page, pageSize));
.listFeaturedPosts(ids, tids, page, pageSize)
.stream()
.map(postMapper::toSummaryDto)
.collect(Collectors.toList());
} }
} }

View File

@@ -48,6 +48,38 @@ public class PostMapper {
return dto; return dto;
} }
public List<PostSummaryDto> toListDtos(List<Post> posts) {
if (posts == null || posts.isEmpty()) {
return List.of();
}
Map<Long, List<User>> participantsMap = commentService.getParticipantsForPosts(posts, 5);
return posts
.stream()
.map(post -> {
PostSummaryDto dto = new PostSummaryDto();
applyListFields(post, dto);
List<User> 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) { public PostDetailDto toDetailDto(Post post, String viewer) {
PostDetailDto dto = new PostDetailDto(); PostDetailDto dto = new PostDetailDto();
applyCommon(post, dto); applyCommon(post, dto);
@@ -61,6 +93,25 @@ public class PostMapper {
return dto; 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) { private void applyCommon(Post post, PostSummaryDto dto) {
dto.setId(post.getId()); dto.setId(post.getId());
dto.setTitle(post.getTitle()); dto.setTitle(post.getTitle());

View File

@@ -25,6 +25,13 @@ public interface CommentRepository extends JpaRepository<Comment, Long> {
@org.springframework.data.repository.query.Param("post") Post post @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<Object[]> findDistinctAuthorsByPostIds(
@org.springframework.data.repository.query.Param("postIds") java.util.List<Long> postIds
);
@org.springframework.data.jpa.repository.Query( @org.springframework.data.jpa.repository.Query(
"SELECT MAX(c.createdAt) FROM Comment c WHERE c.post = :post" "SELECT MAX(c.createdAt) FROM Comment c WHERE c.post = :post"
) )

View File

@@ -19,6 +19,8 @@ public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findByStatusOrderByCreatedAtDesc(PostStatus status, Pageable pageable); List<Post> findByStatusOrderByCreatedAtDesc(PostStatus status, Pageable pageable);
List<Post> findByStatusOrderByViewsDesc(PostStatus status); List<Post> findByStatusOrderByViewsDesc(PostStatus status);
List<Post> findByStatusOrderByViewsDesc(PostStatus status, Pageable pageable); List<Post> findByStatusOrderByViewsDesc(PostStatus status, Pageable pageable);
List<Post> findByStatusOrderByPinnedAtDescViewsDesc(PostStatus status, Pageable pageable);
List<Post> findByStatusOrderByPinnedAtDescLastReplyAtDesc(PostStatus status, Pageable pageable);
List<Post> findByStatusAndCreatedAtGreaterThanEqualOrderByCreatedAtDesc( List<Post> findByStatusAndCreatedAtGreaterThanEqualOrderByCreatedAtDesc(
PostStatus status, PostStatus status,
LocalDateTime createdAt LocalDateTime createdAt
@@ -43,6 +45,16 @@ public interface PostRepository extends JpaRepository<Post, Long> {
PostStatus status, PostStatus status,
Pageable pageable Pageable pageable
); );
List<Post> findByCategoryInAndStatusOrderByPinnedAtDescViewsDesc(
List<Category> categories,
PostStatus status,
Pageable pageable
);
List<Post> findByCategoryInAndStatusOrderByPinnedAtDescLastReplyAtDesc(
List<Category> categories,
PostStatus status,
Pageable pageable
);
List<Post> findDistinctByTagsInAndStatus(List<Tag> tags, PostStatus status); List<Post> findDistinctByTagsInAndStatus(List<Tag> tags, PostStatus status);
List<Post> findDistinctByTagsInAndStatus(List<Tag> tags, PostStatus status, Pageable pageable); List<Post> findDistinctByTagsInAndStatus(List<Tag> tags, PostStatus status, Pageable pageable);
List<Post> findDistinctByTagsInAndStatusOrderByCreatedAtDesc(List<Tag> tags, PostStatus status); List<Post> findDistinctByTagsInAndStatusOrderByCreatedAtDesc(List<Tag> tags, PostStatus status);
@@ -132,6 +144,26 @@ public interface PostRepository extends JpaRepository<Post, Long> {
Pageable pageable 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<Post> findByAllTagsOrderByPinnedAtDescViewsDesc(
@Param("tags") List<Tag> 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<Post> findByAllTagsOrderByPinnedAtDescLastReplyAtDesc(
@Param("tags") List<Tag> tags,
@Param("status") PostStatus status,
@Param("tagCount") long tagCount,
Pageable pageable
);
@Query( @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" "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<Post, Long> {
Pageable pageable 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<Post> findByCategoriesAndAllTagsOrderByPinnedAtDescViewsDesc(
@Param("categories") List<Category> categories,
@Param("tags") List<Tag> 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<Post> findByCategoriesAndAllTagsOrderByPinnedAtDescLastReplyAtDesc(
@Param("categories") List<Category> categories,
@Param("tags") List<Tag> tags,
@Param("status") PostStatus status,
@Param("tagCount") long tagCount,
Pageable pageable
);
@Query( @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" "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"
) )

View File

@@ -21,8 +21,12 @@ import com.openisle.service.NotificationService;
import com.openisle.service.PointService; import com.openisle.service.PointService;
import com.openisle.service.SubscriptionService; import com.openisle.service.SubscriptionService;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -316,6 +320,37 @@ public class CommentService {
return result; return result;
} }
public Map<Long, List<User>> getParticipantsForPosts(List<Post> posts, int limit) {
if (posts == null || posts.isEmpty()) {
return Map.of();
}
Map<Long, LinkedHashSet<User>> map = new HashMap<>();
List<Long> postIds = new ArrayList<>(posts.size());
for (Post post : posts) {
postIds.add(post.getId());
LinkedHashSet<User> 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<User> set = map.get(postId);
if (set != null) {
set.add(author);
}
}
Map<Long, List<User>> result = new HashMap<>(map.size());
for (Map.Entry<Long, LinkedHashSet<User>> entry : map.entrySet()) {
List<User> 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<Comment> getCommentsByIds(java.util.List<Long> ids) { public java.util.List<Comment> getCommentsByIds(java.util.List<Long> ids) {
log.debug("getCommentsByIds called for ids {}", ids); log.debug("getCommentsByIds called for ids {}", ids);
java.util.List<Comment> comments = commentRepository.findAllById(ids); java.util.List<Comment> comments = commentRepository.findAllById(ids);

View File

@@ -339,6 +339,7 @@ public class PostService {
post.setCategory(category); post.setCategory(category);
post.setTags(new HashSet<>(tags)); post.setTags(new HashSet<>(tags));
post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED); post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED);
post.setLastReplyAt(LocalDateTime.now());
// 什么都没设置的情况下默认为ALL // 什么都没设置的情况下默认为ALL
if (Objects.isNull(postVisibleScopeType)) { if (Objects.isNull(postVisibleScopeType)) {
@@ -809,9 +810,10 @@ public class PostService {
boolean hasTags = tagIds != null && !tagIds.isEmpty(); boolean hasTags = tagIds != null && !tagIds.isEmpty();
java.util.List<Post> posts; java.util.List<Post> posts;
Pageable pageable = buildPageable(page, pageSize);
if (!hasCategories && !hasTags) { if (!hasCategories && !hasTags) {
posts = postRepository.findByStatusOrderByViewsDesc(PostStatus.PUBLISHED); posts = postRepository.findByStatusOrderByPinnedAtDescViewsDesc(PostStatus.PUBLISHED, pageable);
} else if (hasCategories) { } else if (hasCategories) {
java.util.List<Category> categories = categoryRepository.findAllById(categoryIds); java.util.List<Category> categories = categoryRepository.findAllById(categoryIds);
if (categories.isEmpty()) { if (categories.isEmpty()) {
@@ -822,16 +824,18 @@ public class PostService {
if (tags.isEmpty()) { if (tags.isEmpty()) {
return java.util.List.of(); return java.util.List.of();
} }
posts = postRepository.findByCategoriesAndAllTagsOrderByViewsDesc( posts = postRepository.findByCategoriesAndAllTagsOrderByPinnedAtDescViewsDesc(
categories, categories,
tags, tags,
PostStatus.PUBLISHED, PostStatus.PUBLISHED,
tags.size() tags.size(),
pageable
); );
} else { } else {
posts = postRepository.findByCategoryInAndStatusOrderByViewsDesc( posts = postRepository.findByCategoryInAndStatusOrderByPinnedAtDescViewsDesc(
categories, categories,
PostStatus.PUBLISHED PostStatus.PUBLISHED,
pageable
); );
} }
} else { } else {
@@ -839,10 +843,15 @@ public class PostService {
if (tags.isEmpty()) { if (tags.isEmpty()) {
return java.util.List.of(); 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<Post> listPostsByLatestReply(Integer page, Integer pageSize) { public List<Post> listPostsByLatestReply(Integer page, Integer pageSize) {
@@ -859,9 +868,13 @@ public class PostService {
boolean hasTags = tagIds != null && !tagIds.isEmpty(); boolean hasTags = tagIds != null && !tagIds.isEmpty();
java.util.List<Post> posts; java.util.List<Post> posts;
Pageable pageable = buildPageable(page, pageSize);
if (!hasCategories && !hasTags) { if (!hasCategories && !hasTags) {
posts = postRepository.findByStatusOrderByCreatedAtDesc(PostStatus.PUBLISHED); posts = postRepository.findByStatusOrderByPinnedAtDescLastReplyAtDesc(
PostStatus.PUBLISHED,
pageable
);
} else if (hasCategories) { } else if (hasCategories) {
java.util.List<Category> categories = categoryRepository.findAllById(categoryIds); java.util.List<Category> categories = categoryRepository.findAllById(categoryIds);
if (categories.isEmpty()) { if (categories.isEmpty()) {
@@ -872,16 +885,18 @@ public class PostService {
if (tags.isEmpty()) { if (tags.isEmpty()) {
return java.util.List.of(); return java.util.List.of();
} }
posts = postRepository.findByCategoriesAndAllTagsOrderByCreatedAtDesc( posts = postRepository.findByCategoriesAndAllTagsOrderByPinnedAtDescLastReplyAtDesc(
categories, categories,
tags, tags,
PostStatus.PUBLISHED, PostStatus.PUBLISHED,
tags.size() tags.size(),
pageable
); );
} else { } else {
posts = postRepository.findByCategoryInAndStatusOrderByCreatedAtDesc( posts = postRepository.findByCategoryInAndStatusOrderByPinnedAtDescLastReplyAtDesc(
categories, categories,
PostStatus.PUBLISHED PostStatus.PUBLISHED,
pageable
); );
} }
} else { } else {
@@ -889,14 +904,15 @@ public class PostService {
if (tags.isEmpty()) { if (tags.isEmpty()) {
return new ArrayList<>(); return new ArrayList<>();
} }
posts = postRepository.findByAllTagsOrderByCreatedAtDesc( posts = postRepository.findByAllTagsOrderByPinnedAtDescLastReplyAtDesc(
tags, tags,
PostStatus.PUBLISHED, PostStatus.PUBLISHED,
tags.size() tags.size(),
pageable
); );
} }
return paginate(sortByPinnedAndLastReply(posts), page, pageSize); return posts;
} }
public List<Post> listPostsByCategories( public List<Post> listPostsByCategories(
@@ -1394,6 +1410,13 @@ public class PostService {
.toList(); .toList();
} }
private Pageable buildPageable(Integer page, Integer pageSize) {
if (page == null || pageSize == null) {
return Pageable.unpaged();
}
return PageRequest.of(page, pageSize);
}
private List<Post> paginate(List<Post> posts, Integer page, Integer pageSize) { private List<Post> paginate(List<Post> posts, Integer page, Integer pageSize) {
if (page == null || pageSize == null) { if (page == null || pageSize == null) {
return posts; return posts;

View File

@@ -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;

View File

@@ -30,62 +30,62 @@ services:
- dev_local_backend - dev_local_backend
- prod - prod
# OpenSearch Service # # OpenSearch Service
opensearch: # opensearch:
user: "1000:1000" # user: "1000:1000"
build: # build:
context: . # context: .
dockerfile: opensearch.Dockerfile # dockerfile: opensearch.Dockerfile
container_name: ${COMPOSE_PROJECT_NAME}-opensearch # container_name: ${COMPOSE_PROJECT_NAME}-opensearch
environment: # environment:
- cluster.name=os-single # - cluster.name=os-single
- node.name=os-node-1 # - node.name=os-node-1
- discovery.type=single-node # - discovery.type=single-node
- bootstrap.memory_lock=true # - bootstrap.memory_lock=true
- OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g # - OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g
- DISABLE_SECURITY_PLUGIN=true # - DISABLE_SECURITY_PLUGIN=true
- cluster.blocks.create_index=false # - cluster.blocks.create_index=false
ulimits: # ulimits:
memlock: { soft: -1, hard: -1 } # memlock: { soft: -1, hard: -1 }
nofile: { soft: 65536, hard: 65536 } # nofile: { soft: 65536, hard: 65536 }
volumes: # volumes:
- opensearch-data:/usr/share/opensearch/data # - opensearch-data:/usr/share/opensearch/data
- opensearch-snapshots:/snapshots # - opensearch-snapshots:/snapshots
ports: # ports:
- "${OPENSEARCH_PORT:-9200}:9200" # - "${OPENSEARCH_PORT:-9200}:9200"
- "${OPENSEARCH_METRICS_PORT:-9600}:9600" # - "${OPENSEARCH_METRICS_PORT:-9600}:9600"
restart: unless-stopped # restart: unless-stopped
healthcheck: # healthcheck:
test: # test:
- CMD-SHELL # - CMD-SHELL
- curl -fsS http://127.0.0.1:9200/_cluster/health >/dev/null # - curl -fsS http://127.0.0.1:9200/_cluster/health >/dev/null
interval: 10s # interval: 10s
timeout: 5s # timeout: 5s
retries: 30 # retries: 30
start_period: 60s # start_period: 60s
networks: # networks:
- openisle-network # - openisle-network
profiles: # profiles:
- dev # - dev
- dev_local_backend # - dev_local_backend
dashboards: # dashboards:
image: opensearchproject/opensearch-dashboards:3.0.0 # image: opensearchproject/opensearch-dashboards:3.0.0
container_name: ${COMPOSE_PROJECT_NAME}-os-dashboards # container_name: ${COMPOSE_PROJECT_NAME}-os-dashboards
environment: # environment:
OPENSEARCH_HOSTS: '["http://opensearch:9200"]' # OPENSEARCH_HOSTS: '["http://opensearch:9200"]'
DISABLE_SECURITY_DASHBOARDS_PLUGIN: "true" # DISABLE_SECURITY_DASHBOARDS_PLUGIN: "true"
ports: # ports:
- "${OPENSEARCH_DASHBOARDS_PORT:-5601}:5601" # - "${OPENSEARCH_DASHBOARDS_PORT:-5601}:5601"
depends_on: # depends_on:
- opensearch # - opensearch
restart: unless-stopped # restart: unless-stopped
networks: # networks:
- openisle-network # - openisle-network
profiles: # profiles:
- dev # - dev
- dev_local_backend # - dev_local_backend
- prod # - prod
rabbitmq: rabbitmq:
image: rabbitmq:3.13-management image: rabbitmq:3.13-management
@@ -200,7 +200,6 @@ services:
- openisle-network - openisle-network
profiles: profiles:
- dev - dev
- dev_local_backend
- prod - prod