From 9ddd74b74bf6513c88874af08290b302a5237a43 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Tue, 21 Dec 2021 23:35:42 +0800 Subject: [PATCH] =?UTF-8?q?Include=20posts=20that=20under=20the=20subcateg?= =?UTF-8?q?ories=20when=20getting=20posts=20b=E2=80=A6=20(#1567)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: include posts that under the subcategories when getting posts by category * refactor: list as tree * fix: can nott query the problem of encrypted category * refactor: post count under the category * fix: list all by parent id --- .../CategoryPostCountProjection.java | 21 -- .../run/halo/app/model/vo/CategoryVO.java | 6 + .../repository/PostCategoryRepository.java | 6 - .../run/halo/app/service/CategoryService.java | 29 ++- .../halo/app/service/PostCategoryService.java | 1 + .../app/service/impl/CategoryServiceImpl.java | 245 ++++++++++-------- .../service/impl/PostCategoryServiceImpl.java | 106 +++++--- .../app/service/impl/PostServiceImpl.java | 8 +- .../app/service/impl/CategoryServiceTest.java | 93 +++++++ 9 files changed, 341 insertions(+), 174 deletions(-) delete mode 100644 src/main/java/run/halo/app/model/projection/CategoryPostCountProjection.java create mode 100644 src/test/java/run/halo/app/service/impl/CategoryServiceTest.java diff --git a/src/main/java/run/halo/app/model/projection/CategoryPostCountProjection.java b/src/main/java/run/halo/app/model/projection/CategoryPostCountProjection.java deleted file mode 100644 index f1acc3d97..000000000 --- a/src/main/java/run/halo/app/model/projection/CategoryPostCountProjection.java +++ /dev/null @@ -1,21 +0,0 @@ -package run.halo.app.model.projection; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Category post count projection. - * - * @author johnniang - * @date 19-4-23 - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CategoryPostCountProjection { - - private Long postCount; - - private Integer categoryId; -} diff --git a/src/main/java/run/halo/app/model/vo/CategoryVO.java b/src/main/java/run/halo/app/model/vo/CategoryVO.java index aef84b74e..5bbbbba7b 100644 --- a/src/main/java/run/halo/app/model/vo/CategoryVO.java +++ b/src/main/java/run/halo/app/model/vo/CategoryVO.java @@ -1,6 +1,8 @@ package run.halo.app.model.vo; +import com.fasterxml.jackson.annotation.JsonIgnore; import java.util.List; +import java.util.Set; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -10,6 +12,7 @@ import run.halo.app.model.dto.CategoryDTO; * Category vo. * * @author johnniang + * @author guqing * @date 3/21/19 */ @Data @@ -17,5 +20,8 @@ import run.halo.app.model.dto.CategoryDTO; @EqualsAndHashCode(callSuper = true) public class CategoryVO extends CategoryDTO { + @JsonIgnore + private Set postIds; + private List children; } diff --git a/src/main/java/run/halo/app/repository/PostCategoryRepository.java b/src/main/java/run/halo/app/repository/PostCategoryRepository.java index 6a3c4ab2a..52828baa4 100644 --- a/src/main/java/run/halo/app/repository/PostCategoryRepository.java +++ b/src/main/java/run/halo/app/repository/PostCategoryRepository.java @@ -7,7 +7,6 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.lang.NonNull; import run.halo.app.model.entity.PostCategory; import run.halo.app.model.enums.PostStatus; -import run.halo.app.model.projection.CategoryPostCountProjection; import run.halo.app.repository.base.BaseRepository; @@ -113,11 +112,6 @@ public interface PostCategoryRepository extends BaseRepository deleteByCategoryId(@NonNull Integer categoryId); - @Query("select new run.halo.app.model.projection.CategoryPostCountProjection(count(pc.postId)" - + ", pc.categoryId) from PostCategory pc group by pc.categoryId") - @NonNull - List findPostCount(); - /** * Finds all post categories by category id list. * diff --git a/src/main/java/run/halo/app/service/CategoryService.java b/src/main/java/run/halo/app/service/CategoryService.java index e6b42d6e0..a0e37fa96 100755 --- a/src/main/java/run/halo/app/service/CategoryService.java +++ b/src/main/java/run/halo/app/service/CategoryService.java @@ -1,5 +1,6 @@ package run.halo.app.service; +import io.swagger.models.auth.In; import java.util.Collection; import java.util.List; import org.springframework.data.domain.Sort; @@ -16,6 +17,7 @@ import run.halo.app.service.base.CrudService; * * @author johnniang * @author ryanwang + * @author guqing * @date 2019-03-14 */ @Transactional(readOnly = true) @@ -31,7 +33,16 @@ public interface CategoryService extends CrudService { List listAsTree(@NonNull Sort sort); /** - * Get category by slug + * Build category full path. + * + * @param slug category slug name. + * @return full path of category. + */ + @NonNull + String buildCategoryFullPath(@NonNull String slug); + + /** + * Get category by slug. * * @param slug slug * @return Category @@ -90,6 +101,14 @@ public interface CategoryService extends CrudService { */ List listByParentId(@NonNull Integer id); + /** + * List all child categories and current category by parent id. + * + * @param id parent id. + * @return a list of category that contain current id. + */ + List listAllByParentId(@NonNull Integer id); + /** * List all category not encrypt. * @@ -153,4 +172,12 @@ public interface CategoryService extends CrudService { */ @NonNull Boolean categoryHasEncrypt(Integer categoryId); + + /** + * Use categories to build a category tree. + * + * @param categories categories to build a tree. + * @return a tree of category. + */ + List listToTree(List categories); } diff --git a/src/main/java/run/halo/app/service/PostCategoryService.java b/src/main/java/run/halo/app/service/PostCategoryService.java index 3869b8822..217cd16ea 100644 --- a/src/main/java/run/halo/app/service/PostCategoryService.java +++ b/src/main/java/run/halo/app/service/PostCategoryService.java @@ -22,6 +22,7 @@ import run.halo.app.service.base.CrudService; * * @author johnniang * @author ryanwang + * @author guqing * @date 2019-03-19 */ public interface PostCategoryService extends CrudService { diff --git a/src/main/java/run/halo/app/service/impl/CategoryServiceImpl.java b/src/main/java/run/halo/app/service/impl/CategoryServiceImpl.java index 6ef39ef1b..5302f1f3c 100644 --- a/src/main/java/run/halo/app/service/impl/CategoryServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/CategoryServiceImpl.java @@ -3,6 +3,7 @@ package run.halo.app.service.impl; import static run.halo.app.model.support.HaloConst.URL_SEPARATOR; import com.google.common.base.Objects; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -10,6 +11,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Queue; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -20,6 +22,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -51,6 +54,7 @@ import run.halo.app.utils.ServiceUtils; * * @author ryanwang * @author johnniang + * @author guqing * @date 2019-03-14 */ @Slf4j @@ -132,93 +136,43 @@ public class CategoryServiceImpl extends AbstractCrudService if (CollectionUtils.isEmpty(categories)) { return Collections.emptyList(); } + // Do not return category password. + desensitizePassword(categories); - // Create top category - CategoryVO topLevelCategory = createTopLevelCategory(); - - // Concrete the tree - concreteTree(topLevelCategory, categories, false); - - return topLevelCategory.getChildren(); + return listToTree(categories); } - /** - * Concrete category tree. - * - * @param parentCategory parent category vo must not be null - * @param categories a list of category - * @param fillPassword whether to fill in the password - */ - private void concreteTree( - CategoryVO parentCategory, - List categories, - Boolean fillPassword - ) { - Assert.notNull(parentCategory, "Parent category must not be null"); - - if (CollectionUtils.isEmpty(categories)) { - return; - } - - // Get children for removing after - List children = categories.stream() - .filter(category -> Objects.equal(parentCategory.getId(), category.getParentId())) - .collect(Collectors.toList()); - - children.forEach(category -> { - // Convert to child category vo - CategoryVO child = new CategoryVO().convertFrom(category); - // Init children if absent - if (parentCategory.getChildren() == null) { - parentCategory.setChildren(new LinkedList<>()); - } - - StringBuilder fullPath = new StringBuilder(); - - if (optionService.isEnabledAbsolutePath()) { - fullPath.append(optionService.getBlogBaseUrl()); - } - - fullPath.append(URL_SEPARATOR) - .append(optionService.getCategoriesPrefix()) - .append(URL_SEPARATOR) - .append(child.getSlug()) - .append(optionService.getPathSuffix()); - - child.setFullPath(fullPath.toString()); - - if (!fillPassword) { - child.setPassword(null); - } - - // Add child - parentCategory.getChildren().add(child); + private void desensitizePassword(List categories) { + Assert.notNull(categories, "The categories must not be null."); + categories.forEach(category -> { + category.setPassword(null); }); - - // Remove all child categories - categories.removeAll(children); - - // Foreach children vos - if (!CollectionUtils.isEmpty(parentCategory.getChildren())) { - parentCategory.getChildren() - .forEach(childCategory -> concreteTree(childCategory, categories, fillPassword)); - } } - /** - * Creates a top level category. - * - * @return top level category with id 0 - */ @NonNull - private CategoryVO createTopLevelCategory() { - CategoryVO topCategory = new CategoryVO(); - // Set default value - topCategory.setId(0); - topCategory.setChildren(new LinkedList<>()); - topCategory.setParentId(-1); + @Override + public String buildCategoryFullPath(@NonNull String slug) { + Assert.notNull(slug, "The slug must not be null."); + StringBuilder fullPath = new StringBuilder(); - return topCategory; + if (optionService.isEnabledAbsolutePath()) { + fullPath.append(optionService.getBlogBaseUrl()); + } + + fullPath.append(URL_SEPARATOR) + .append(optionService.getCategoriesPrefix()) + .append(URL_SEPARATOR) + .append(slug) + .append(optionService.getPathSuffix()); + return fullPath.toString(); + } + + @NonNull + private CategoryVO convertToCategoryVo(Category category) { + Assert.notNull(category, "The category must not be null."); + CategoryVO categoryVo = new CategoryVO().convertFrom(category); + categoryVo.setFullPath(buildCategoryFullPath(categoryVo.getSlug())); + return categoryVo; } @Override @@ -328,32 +282,79 @@ public class CategoryServiceImpl extends AbstractCrudService } @Override - public List listByParentId(Integer id) { + public List listByParentId(@NonNull Integer id) { Assert.notNull(id, "Parent id must not be null"); return categoryRepository.findByParentId(id); } + @Override + public List listAllByParentId(@NonNull Integer id) { + Assert.notNull(id, "Parent id must not be null"); + List categories = super.listAll(Sort.by(Order.asc("name"))); + List categoryTree = listToTree(categories); + return findCategoryTreeNodeById(categoryTree, id) + .map(this::walkCategoryTree) + .orElse(Collections.emptyList()); + } + + /** + * Walk a category tree with root node. + * + * @param root a root node of category tree. + * @return a flattened category list + */ + @NonNull + private List walkCategoryTree(CategoryVO root) { + Assert.notNull(root, "The category 'root' must not be null"); + List categories = new LinkedList<>(); + Queue queue = new ArrayDeque<>(); + queue.add(root); + while (!queue.isEmpty()) { + CategoryVO categoryNode = queue.poll(); + + Category category = new Category(); + BeanUtils.updateProperties(categoryNode, category); + categories.add(category); + + if (HaloUtils.isNotEmpty(categoryNode.getChildren())) { + queue.addAll(categoryNode.getChildren()); + } + } + return categories; + } + + /** + * Find the node with id equal to categoryId from the multi-tree of category. + * + * @param categoryVos source category multi-tree vos to walk. + * @param categoryId category id to be found. + * @return the root node of the subtree. + */ + private Optional findCategoryTreeNodeById(List categoryVos, + Integer categoryId) { + Assert.notNull(categoryId, "categoryId id must not be null"); + Queue queue = new ArrayDeque<>(categoryVos); + while (!queue.isEmpty()) { + CategoryVO category = queue.poll(); + if (Objects.equal(category.getId(), categoryId)) { + return Optional.of(category); + } + if (HaloUtils.isNotEmpty(category.getChildren())) { + queue.addAll(category.getChildren()); + } + } + return Optional.empty(); + } + @Override public CategoryDTO convertTo(Category category) { Assert.notNull(category, "Category must not be null"); - CategoryDTO categoryDTO = new CategoryDTO().convertFrom(category); + CategoryDTO categoryDto = new CategoryDTO().convertFrom(category); - StringBuilder fullPath = new StringBuilder(); + categoryDto.setFullPath(buildCategoryFullPath(category.getSlug())); - if (optionService.isEnabledAbsolutePath()) { - fullPath.append(optionService.getBlogBaseUrl()); - } - - fullPath.append(URL_SEPARATOR) - .append(optionService.getCategoriesPrefix()) - .append(URL_SEPARATOR) - .append(category.getSlug()) - .append(optionService.getPathSuffix()); - - categoryDTO.setFullPath(fullPath.toString()); - - return categoryDTO; + return categoryDto; } @Override @@ -373,19 +374,15 @@ public class CategoryServiceImpl extends AbstractCrudService return Collections.emptyList(); } - // Concrete the tree - CategoryVO topLevelCategory = createTopLevelCategory(); - - concreteTree(topLevelCategory, categories, true); - - List childrenList = topLevelCategory.getChildren(); + // list to tree, no password desensitise is required here + List categoryTree = listToTree(categories); // filter encrypt category - doFilterEncryptCategory(childrenList); + doFilterEncryptCategory(categoryTree); List collectorList = new ArrayList<>(); - collectAllChild(collectorList, childrenList, true); + collectAllChild(collectorList, categoryTree, true); for (Category category : collectorList) { category.setPassword(null); @@ -551,14 +548,13 @@ public class CategoryServiceImpl extends AbstractCrudService * @param category need encrypt category */ private void doEncryptPost(Category category) { - CategoryVO topLevelCategory = createTopLevelCategory(); - - concreteTree(topLevelCategory, super.listAll(), true); + // list to tree with password + List categoryTree = listToTree(super.listAll()); List collectorList = new ArrayList<>(); collectAllChildByCategoryId(collectorList, - topLevelCategory.getChildren(), category.getId(), true); + categoryTree, category.getId(), true); Optional.of(collectorList.stream().map(Category::getId).collect(Collectors.toList())) .map(categoryIdList -> { @@ -594,16 +590,14 @@ public class CategoryServiceImpl extends AbstractCrudService // If the parent category is encrypted, there is no need to update the encryption status return; } - - CategoryVO topLevelCategory = createTopLevelCategory(); - - concreteTree(topLevelCategory, allCategoryList, true); + // with password + List categoryTree = listToTree(allCategoryList); List collectorList = new ArrayList<>(); // Only collect unencrypted sub-categories under the category. collectAllChildByCategoryId(collectorList, - topLevelCategory.getChildren(), category.getId(), false); + categoryTree, category.getId(), false); // Collect the currently decrypted category collectorList.add(category); @@ -637,6 +631,33 @@ public class CategoryServiceImpl extends AbstractCrudService return doCategoryHasEncrypt(idToCategoryMap, categoryId); } + @Override + public List listToTree(List categories) { + Assert.notNull(categories, "The categories must not be null."); + // batch convert category to categoryVo + List categoryVoList = categories.stream() + .map(this::convertToCategoryVo) + .collect(Collectors.toList()); + + // build a tree, the time complexity is O(n) + Map> parentIdMap = categoryVoList.stream() + .collect(Collectors.groupingBy(CategoryVO::getParentId)); + + // set children + categoryVoList.forEach(category -> { + List children = parentIdMap.get(category.getId()); + if (CollectionUtils.isEmpty(children)) { + category.setChildren(Collections.emptyList()); + } else { + category.setChildren(children); + } + }); + + return categoryVoList.stream() + .filter(category -> category.getParentId() == null || category.getParentId() == 0) + .collect(Collectors.toList()); + } + /** * Find whether the parent category is encrypted. * diff --git a/src/main/java/run/halo/app/service/impl/PostCategoryServiceImpl.java b/src/main/java/run/halo/app/service/impl/PostCategoryServiceImpl.java index 8fc74383f..220d0b8a6 100644 --- a/src/main/java/run/halo/app/service/impl/PostCategoryServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/PostCategoryServiceImpl.java @@ -1,21 +1,25 @@ package run.halo.app.service.impl; -import static run.halo.app.model.support.HaloConst.URL_SEPARATOR; - +import java.util.ArrayDeque; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Queue; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -25,13 +29,14 @@ import run.halo.app.model.entity.Category; import run.halo.app.model.entity.Post; import run.halo.app.model.entity.PostCategory; import run.halo.app.model.enums.PostStatus; -import run.halo.app.model.projection.CategoryPostCountProjection; +import run.halo.app.model.vo.CategoryVO; import run.halo.app.repository.PostCategoryRepository; import run.halo.app.repository.PostRepository; import run.halo.app.service.CategoryService; import run.halo.app.service.OptionService; import run.halo.app.service.PostCategoryService; import run.halo.app.service.base.AbstractCrudService; +import run.halo.app.utils.HaloUtils; import run.halo.app.utils.ServiceUtils; /** @@ -304,44 +309,80 @@ public class PostCategoryServiceImpl extends AbstractCrudService listCategoryWithPostCountDto( - Sort sort, boolean queryEncryptCategory) { + public List listCategoryWithPostCountDto(@NonNull Sort sort, + boolean queryEncryptCategory) { Assert.notNull(sort, "Sort info must not be null"); List categories = categoryService.listAll(sort, queryEncryptCategory); - - // Query category post count - Map categoryPostCountMap = ServiceUtils - .convertToMap(postCategoryRepository.findPostCount(), - CategoryPostCountProjection::getCategoryId, - CategoryPostCountProjection::getPostCount); + List categoryTreeVo = categoryService.listToTree(categories); + populatePostIds(categoryTreeVo); // Convert and return - return categories.stream() - .map(category -> { - // Create category post count dto - CategoryWithPostCountDTO categoryWithPostCountDTO = - new CategoryWithPostCountDTO().convertFrom(category); - // Set post count - categoryWithPostCountDTO - .setPostCount(categoryPostCountMap.getOrDefault(category.getId(), 0L)); + return flatTreeToList(categoryTreeVo); + } - StringBuilder fullPath = new StringBuilder(); + private List flatTreeToList(List categoryTree) { + Assert.notNull(categoryTree, "The categoryTree must not be null."); + List result = new LinkedList<>(); + walkCategoryTree(categoryTree, category -> { + CategoryWithPostCountDTO categoryWithPostCountDto = + new CategoryWithPostCountDTO(); + BeanUtils.copyProperties(category, categoryWithPostCountDto); + String fullPath = categoryService.buildCategoryFullPath(category.getSlug()); + categoryWithPostCountDto.setFullPath(fullPath); + // populate post count. + int postCount = Objects.requireNonNullElse(category.getPostIds(), + Collections.emptySet()).size(); + categoryWithPostCountDto.setPostCount((long) postCount); + result.add(categoryWithPostCountDto); + }); + return result; + } - if (optionService.isEnabledAbsolutePath()) { - fullPath.append(optionService.getBlogBaseUrl()); - } + private void populatePostIds(List categoryTree) { + Assert.notNull(categoryTree, "The categoryTree must not be null."); + Map> categoryPostIdsMap = postCategoryRepository.findAll() + .stream() + .collect(Collectors.groupingBy(PostCategory::getCategoryId, + Collectors.mapping(PostCategory::getPostId, Collectors.toSet()))); - fullPath.append(URL_SEPARATOR) - .append(optionService.getCategoriesPrefix()) - .append(URL_SEPARATOR) - .append(category.getSlug()) - .append(optionService.getPathSuffix()); + walkCategoryTree(categoryTree, category -> { + // Set post count + Set postIds = + categoryPostIdsMap.getOrDefault(category.getId(), new LinkedHashSet<>()); + category.setPostIds(postIds); + }); + CategoryVO categoryTreeRootNode = new CategoryVO(); + categoryTreeRootNode.setChildren(categoryTree); + mergePostIdsFromBottomToTop(categoryTreeRootNode); + } - categoryWithPostCountDTO.setFullPath(fullPath.toString()); + private void mergePostIdsFromBottomToTop(CategoryVO root) { + if (root == null) { + return; + } + List children = root.getChildren(); + if (CollectionUtils.isEmpty(children)) { + return; + } + for (CategoryVO category : children) { + mergePostIdsFromBottomToTop(category); + if (root.getPostIds() == null) { + root.setPostIds(new LinkedHashSet<>()); + } + // merge post ids. + root.getPostIds().addAll(category.getPostIds()); + } + } - return categoryWithPostCountDTO; - }) - .collect(Collectors.toList()); + private void walkCategoryTree(List categoryTree, Consumer consumer) { + Queue queue = new ArrayDeque<>(categoryTree); + while (!queue.isEmpty()) { + CategoryVO category = queue.poll(); + consumer.accept(category); + if (HaloUtils.isNotEmpty(category.getChildren())) { + queue.addAll(category.getChildren()); + } + } } @Override @@ -349,4 +390,5 @@ public class PostCategoryServiceImpl extends AbstractCrudService implements PostSe } if (postQuery.getCategoryId() != null) { + List categoryIds = + categoryService.listAllByParentId(postQuery.getCategoryId()) + .stream() + .map(Category::getId) + .collect(Collectors.toList()); Subquery postSubquery = query.subquery(Post.class); Root postCategoryRoot = postSubquery.from(PostCategory.class); postSubquery.select(postCategoryRoot.get("postId")); postSubquery.where( criteriaBuilder.equal(root.get("id"), postCategoryRoot.get("postId")), - criteriaBuilder - .equal(postCategoryRoot.get("categoryId"), postQuery.getCategoryId())); + postCategoryRoot.get("categoryId").in(categoryIds)); predicates.add(criteriaBuilder.exists(postSubquery)); } diff --git a/src/test/java/run/halo/app/service/impl/CategoryServiceTest.java b/src/test/java/run/halo/app/service/impl/CategoryServiceTest.java new file mode 100644 index 000000000..c96c41ed4 --- /dev/null +++ b/src/test/java/run/halo/app/service/impl/CategoryServiceTest.java @@ -0,0 +1,93 @@ +package run.halo.app.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.List; +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.ActiveProfiles; +import run.halo.app.model.entity.Category; +import run.halo.app.model.vo.CategoryVO; +import run.halo.app.utils.JsonUtils; + +/** + * Category service test. + * + * @author guqing + * @date 2021-12-04 + */ +@ActiveProfiles("test") +@SpringBootTest +public class CategoryServiceTest { + + @Autowired + private CategoryServiceImpl categoryService; + + @Test + void listToTree() throws JsonProcessingException { + List categories = mockCategories(); + List categoryVoList = categoryService.listToTree(categories); + assertEquals( + "[{\"id\":1,\"name\":\"分类-1\",\"slug\":\"category1\",\"description\":null," + + "\"thumbnail\":null,\"parentId\":0,\"password\":null,\"createTime\":null," + + "\"fullPath\":\"http://127.0.0.1:8090/categories/category1\"," + + "\"children\":[]},{\"id\":2,\"name\":\"分类-2\",\"slug\":\"category2\"," + + "\"description\":null,\"thumbnail\":null,\"parentId\":0,\"password\":null," + + "\"createTime\":null,\"fullPath\":\"http://127.0.0.1:8090/categories/category2\"," + + "\"children\":[{\"id\":3,\"name\":\"分类-2-1\",\"slug\":\"category21\"," + + "\"description\":null,\"thumbnail\":null,\"parentId\":2,\"password\":null," + + "\"createTime\":null,\"fullPath\":\"http://127.0.0.1:8090/categories/category21\"," + + "\"children\":[{\"id\":5,\"name\":\"分类-2-1-1\",\"slug\":\"category211\"," + + "\"description\":null,\"thumbnail\":null,\"parentId\":3,\"password\":null," + + "\"createTime\":null,\"fullPath\":\"http://127.0.0.1:8090/categories/category211\"," + + "\"children\":[{\"id\":6,\"name\":\"分类-2-1-1-1\",\"slug\":\"category2111\"," + + "\"description\":null,\"thumbnail\":null,\"parentId\":5,\"password\":null," + + "\"createTime\":null,\"fullPath\":\"http://127.0.0.1:8090/categories/category2111\"," + + "\"children\":[]}]}]},{\"id\":4,\"name\":\"分类-2-2\",\"slug\":\"category22\"," + + "\"description\":null,\"thumbnail\":null,\"parentId\":2,\"password\":null," + + "\"createTime\":null,\"fullPath\":\"http://127.0.0.1:8090/categories/category22\"," + + "\"children\":[]}]}]", + JsonUtils.objectToJson(categoryVoList)); + } + + private List mockCategories() { + Category category1 = new Category(); + category1.setId(1); + category1.setName("分类-1"); + category1.setSlug("category1"); + category1.setParentId(0); + + Category category2 = new Category(); + category2.setId(2); + category2.setName("分类-2"); + category2.setSlug("category2"); + category2.setParentId(0); + + Category category3 = new Category(); + category3.setId(3); + category3.setName("分类-2-1"); + category3.setSlug("category21"); + category3.setParentId(2); + + Category category4 = new Category(); + category4.setId(4); + category4.setName("分类-2-2"); + category4.setSlug("category22"); + category4.setParentId(2); + + Category category5 = new Category(); + category5.setId(5); + category5.setName("分类-2-1-1"); + category5.setSlug("category211"); + category5.setParentId(3); + + Category category6 = new Category(); + category6.setId(6); + category6.setName("分类-2-1-1-1"); + category6.setSlug("category2111"); + category6.setParentId(5); + return List.of(category1, category2, category3, category4, category5, category6); + } +}