mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-03-03 02:20:49 +08:00
Add paginated tag fetching for backend and UI
This commit is contained in:
@@ -2,6 +2,7 @@ package com.openisle.controller;
|
||||
|
||||
import com.openisle.dto.PostSummaryDto;
|
||||
import com.openisle.dto.TagDto;
|
||||
import com.openisle.dto.TagPageResponse;
|
||||
import com.openisle.dto.TagRequest;
|
||||
import com.openisle.mapper.PostMapper;
|
||||
import com.openisle.mapper.TagMapper;
|
||||
@@ -19,8 +20,13 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@@ -96,24 +102,45 @@ public class TagController {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "List of tags",
|
||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class)))
|
||||
content = @Content(schema = @Schema(implementation = TagPageResponse.class))
|
||||
)
|
||||
public List<TagDto> list(
|
||||
public TagPageResponse list(
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "limit", required = false) Integer limit
|
||||
@RequestParam(value = "limit", required = false) Integer limit,
|
||||
@RequestParam(value = "page", required = false) Integer page,
|
||||
@RequestParam(value = "pageSize", required = false) Integer pageSize
|
||||
) {
|
||||
List<Tag> tags = tagService.searchTags(keyword);
|
||||
int resolvedPageSize = Optional.ofNullable(pageSize)
|
||||
.filter(s -> s > 0)
|
||||
.or(() -> Optional.ofNullable(limit).filter(l -> l > 0))
|
||||
.orElse(20);
|
||||
int resolvedPage = Optional.ofNullable(page)
|
||||
.filter(p -> p >= 0)
|
||||
.orElse(0);
|
||||
|
||||
Pageable pageable = PageRequest.of(
|
||||
resolvedPage,
|
||||
resolvedPageSize,
|
||||
Sort.by(Sort.Direction.DESC, "createdAt")
|
||||
);
|
||||
Page<Tag> tagPage = tagService.searchTags(keyword, pageable);
|
||||
List<Tag> tags = tagPage.getContent();
|
||||
List<Long> tagIds = tags.stream().map(Tag::getId).toList();
|
||||
Map<Long, Long> postCntByTagIds = postService.countPostsByTagIds(tagIds);
|
||||
Map<Long, Long> postCntByTagIds = Optional.ofNullable(
|
||||
postService.countPostsByTagIds(tagIds)
|
||||
).orElseGet(Map::of);
|
||||
List<TagDto> dtos = tags
|
||||
.stream()
|
||||
.map(t -> tagMapper.toDto(t, postCntByTagIds.getOrDefault(t.getId(), 0L)))
|
||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||
.collect(Collectors.toList());
|
||||
if (limit != null && limit > 0 && dtos.size() > limit) {
|
||||
return dtos.subList(0, limit);
|
||||
}
|
||||
return dtos;
|
||||
|
||||
return new TagPageResponse(
|
||||
dtos,
|
||||
tagPage.getNumber(),
|
||||
tagPage.getSize(),
|
||||
tagPage.getTotalElements(),
|
||||
tagPage.hasNext()
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
|
||||
18
backend/src/main/java/com/openisle/dto/TagPageResponse.java
Normal file
18
backend/src/main/java/com/openisle/dto/TagPageResponse.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.openisle.dto;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TagPageResponse {
|
||||
|
||||
private List<TagDto> items;
|
||||
private int page;
|
||||
private int pageSize;
|
||||
private long total;
|
||||
private boolean hasNext;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.openisle.model.Tag;
|
||||
import com.openisle.model.User;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
@@ -12,6 +13,8 @@ public interface TagRepository extends JpaRepository<Tag, Long> {
|
||||
List<Tag> findByApproved(boolean approved);
|
||||
List<Tag> findByApprovedTrue();
|
||||
List<Tag> findByNameContainingIgnoreCaseAndApprovedTrue(String keyword);
|
||||
Page<Tag> findByApprovedTrue(Pageable pageable);
|
||||
Page<Tag> findByNameContainingIgnoreCaseAndApprovedTrue(String keyword, Pageable pageable);
|
||||
|
||||
List<Tag> findByCreatorOrderByCreatedAtDesc(User creator, Pageable pageable);
|
||||
List<Tag> findByCreator(User creator);
|
||||
|
||||
@@ -9,8 +9,11 @@ import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@@ -113,16 +116,37 @@ public class TagService {
|
||||
* @param keyword
|
||||
* @return
|
||||
*/
|
||||
public List<Tag> searchTags(String keyword) {
|
||||
return searchTags(keyword, Pageable.unpaged()).getContent();
|
||||
}
|
||||
|
||||
@Cacheable(
|
||||
value = CachingConfig.TAG_CACHE_NAME,
|
||||
key = "'searchTags:' + (#keyword ?: '')" //keyword为null的场合返回空
|
||||
key = "'searchTags:' + (#keyword ?: '') + ':' + (#pageable != null && #pageable.isPaged() ? #pageable.pageNumber : 0) + ':' + (#pageable != null && #pageable.isPaged() ? #pageable.pageSize : 0)"
|
||||
)
|
||||
public List<Tag> searchTags(String keyword) {
|
||||
if (keyword == null || keyword.isBlank()) {
|
||||
return tagRepository.findByApprovedTrue();
|
||||
public Page<Tag> searchTags(String keyword, Pageable pageable) {
|
||||
Pageable effectivePageable = pageable != null ? pageable : Pageable.unpaged();
|
||||
boolean hasKeyword = keyword != null && !keyword.isBlank();
|
||||
|
||||
if (!effectivePageable.isPaged()) {
|
||||
List<Tag> tags = hasKeyword
|
||||
? tagRepository.findByNameContainingIgnoreCaseAndApprovedTrue(keyword)
|
||||
: tagRepository.findByApprovedTrue();
|
||||
return new PageImpl<>(tags, Pageable.unpaged(), tags.size());
|
||||
}
|
||||
|
||||
return tagRepository.findByNameContainingIgnoreCaseAndApprovedTrue(keyword);
|
||||
Pageable sortedPageable = effectivePageable;
|
||||
if (effectivePageable.getSort().isUnsorted()) {
|
||||
sortedPageable = PageRequest.of(
|
||||
effectivePageable.getPageNumber(),
|
||||
effectivePageable.getPageSize(),
|
||||
Sort.by(Sort.Direction.DESC, "createdAt")
|
||||
);
|
||||
}
|
||||
|
||||
return hasKeyword
|
||||
? tagRepository.findByNameContainingIgnoreCaseAndApprovedTrue(keyword, sortedPageable)
|
||||
: tagRepository.findByApprovedTrue(sortedPageable);
|
||||
}
|
||||
|
||||
public List<Tag> getRecentTagsByUser(String username, int limit) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.openisle.repository.UserRepository;
|
||||
import com.openisle.service.PostService;
|
||||
import com.openisle.service.TagService;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -19,6 +20,8 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@@ -81,16 +84,26 @@ class TagControllerTest {
|
||||
t.setDescription("d2");
|
||||
t.setIcon("i2");
|
||||
t.setSmallIcon("s2");
|
||||
Mockito.when(tagService.searchTags(null)).thenReturn(List.of(t));
|
||||
Mockito.when(tagService.searchTags(Mockito.isNull(), Mockito.any(Pageable.class))).thenAnswer(
|
||||
invocation -> {
|
||||
Pageable pageable = invocation.getArgument(1, Pageable.class);
|
||||
return new PageImpl<>(List.of(t), pageable, 1);
|
||||
}
|
||||
);
|
||||
Mockito.when(postService.countPostsByTagIds(List.of(2L))).thenReturn(Map.of(2L, 3L));
|
||||
|
||||
mockMvc
|
||||
.perform(get("/api/tags"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].name").value("spring"))
|
||||
.andExpect(jsonPath("$[0].description").value("d2"))
|
||||
.andExpect(jsonPath("$[0].icon").value("i2"))
|
||||
.andExpect(jsonPath("$[0].smallIcon").value("s2"));
|
||||
.andExpect(jsonPath("$.items[0].name").value("spring"))
|
||||
.andExpect(jsonPath("$.items[0].description").value("d2"))
|
||||
.andExpect(jsonPath("$.items[0].icon").value("i2"))
|
||||
.andExpect(jsonPath("$.items[0].smallIcon").value("s2"))
|
||||
.andExpect(jsonPath("$.items[0].count").value(3))
|
||||
.andExpect(jsonPath("$.page").value(0))
|
||||
.andExpect(jsonPath("$.pageSize").value(20))
|
||||
.andExpect(jsonPath("$.hasNext").value(false))
|
||||
.andExpect(jsonPath("$.total").value(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user