mirror of https://github.com/halo-dev/halo
Include posts that under the subcategories when getting posts b… (#1567)
* 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 idpull/1604/head
parent
f2395b7b5f
commit
9ddd74b74b
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,6 +1,8 @@
|
||||||
package run.halo.app.model.vo;
|
package run.halo.app.model.vo;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
@ -10,6 +12,7 @@ import run.halo.app.model.dto.CategoryDTO;
|
||||||
* Category vo.
|
* Category vo.
|
||||||
*
|
*
|
||||||
* @author johnniang
|
* @author johnniang
|
||||||
|
* @author guqing
|
||||||
* @date 3/21/19
|
* @date 3/21/19
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@ -17,5 +20,8 @@ import run.halo.app.model.dto.CategoryDTO;
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class CategoryVO extends CategoryDTO {
|
public class CategoryVO extends CategoryDTO {
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
private Set<Integer> postIds;
|
||||||
|
|
||||||
private List<CategoryVO> children;
|
private List<CategoryVO> children;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import run.halo.app.model.entity.PostCategory;
|
import run.halo.app.model.entity.PostCategory;
|
||||||
import run.halo.app.model.enums.PostStatus;
|
import run.halo.app.model.enums.PostStatus;
|
||||||
import run.halo.app.model.projection.CategoryPostCountProjection;
|
|
||||||
import run.halo.app.repository.base.BaseRepository;
|
import run.halo.app.repository.base.BaseRepository;
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,11 +112,6 @@ public interface PostCategoryRepository extends BaseRepository<PostCategory, Int
|
||||||
@NonNull
|
@NonNull
|
||||||
List<PostCategory> deleteByCategoryId(@NonNull Integer categoryId);
|
List<PostCategory> 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<CategoryPostCountProjection> findPostCount();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds all post categories by category id list.
|
* Finds all post categories by category id list.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.service;
|
package run.halo.app.service;
|
||||||
|
|
||||||
|
import io.swagger.models.auth.In;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
|
@ -16,6 +17,7 @@ import run.halo.app.service.base.CrudService;
|
||||||
*
|
*
|
||||||
* @author johnniang
|
* @author johnniang
|
||||||
* @author ryanwang
|
* @author ryanwang
|
||||||
|
* @author guqing
|
||||||
* @date 2019-03-14
|
* @date 2019-03-14
|
||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
|
@ -31,7 +33,16 @@ public interface CategoryService extends CrudService<Category, Integer> {
|
||||||
List<CategoryVO> listAsTree(@NonNull Sort sort);
|
List<CategoryVO> 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
|
* @param slug slug
|
||||||
* @return Category
|
* @return Category
|
||||||
|
@ -90,6 +101,14 @@ public interface CategoryService extends CrudService<Category, Integer> {
|
||||||
*/
|
*/
|
||||||
List<Category> listByParentId(@NonNull Integer id);
|
List<Category> 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<Category> listAllByParentId(@NonNull Integer id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all category not encrypt.
|
* List all category not encrypt.
|
||||||
*
|
*
|
||||||
|
@ -153,4 +172,12 @@ public interface CategoryService extends CrudService<Category, Integer> {
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
Boolean categoryHasEncrypt(Integer categoryId);
|
Boolean categoryHasEncrypt(Integer categoryId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use <code>categories</code> to build a category tree.
|
||||||
|
*
|
||||||
|
* @param categories categories to build a tree.
|
||||||
|
* @return a tree of category.
|
||||||
|
*/
|
||||||
|
List<CategoryVO> listToTree(List<Category> categories);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import run.halo.app.service.base.CrudService;
|
||||||
*
|
*
|
||||||
* @author johnniang
|
* @author johnniang
|
||||||
* @author ryanwang
|
* @author ryanwang
|
||||||
|
* @author guqing
|
||||||
* @date 2019-03-19
|
* @date 2019-03-19
|
||||||
*/
|
*/
|
||||||
public interface PostCategoryService extends CrudService<PostCategory, Integer> {
|
public interface PostCategoryService extends CrudService<PostCategory, Integer> {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package run.halo.app.service.impl;
|
||||||
import static run.halo.app.model.support.HaloConst.URL_SEPARATOR;
|
import static run.halo.app.model.support.HaloConst.URL_SEPARATOR;
|
||||||
|
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -10,6 +11,7 @@ import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Queue;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
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.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.domain.Sort.Order;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
@ -51,6 +54,7 @@ import run.halo.app.utils.ServiceUtils;
|
||||||
*
|
*
|
||||||
* @author ryanwang
|
* @author ryanwang
|
||||||
* @author johnniang
|
* @author johnniang
|
||||||
|
* @author guqing
|
||||||
* @date 2019-03-14
|
* @date 2019-03-14
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ -132,47 +136,23 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
|
||||||
if (CollectionUtils.isEmpty(categories)) {
|
if (CollectionUtils.isEmpty(categories)) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
// Do not return category password.
|
||||||
|
desensitizePassword(categories);
|
||||||
|
|
||||||
// Create top category
|
return listToTree(categories);
|
||||||
CategoryVO topLevelCategory = createTopLevelCategory();
|
|
||||||
|
|
||||||
// Concrete the tree
|
|
||||||
concreteTree(topLevelCategory, categories, false);
|
|
||||||
|
|
||||||
return topLevelCategory.getChildren();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void desensitizePassword(List<Category> categories) {
|
||||||
* Concrete category tree.
|
Assert.notNull(categories, "The categories must not be null.");
|
||||||
*
|
categories.forEach(category -> {
|
||||||
* @param parentCategory parent category vo must not be null
|
category.setPassword(null);
|
||||||
* @param categories a list of category
|
});
|
||||||
* @param fillPassword whether to fill in the password
|
|
||||||
*/
|
|
||||||
private void concreteTree(
|
|
||||||
CategoryVO parentCategory,
|
|
||||||
List<Category> categories,
|
|
||||||
Boolean fillPassword
|
|
||||||
) {
|
|
||||||
Assert.notNull(parentCategory, "Parent category must not be null");
|
|
||||||
|
|
||||||
if (CollectionUtils.isEmpty(categories)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get children for removing after
|
|
||||||
List<Category> 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<>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String buildCategoryFullPath(@NonNull String slug) {
|
||||||
|
Assert.notNull(slug, "The slug must not be null.");
|
||||||
StringBuilder fullPath = new StringBuilder();
|
StringBuilder fullPath = new StringBuilder();
|
||||||
|
|
||||||
if (optionService.isEnabledAbsolutePath()) {
|
if (optionService.isEnabledAbsolutePath()) {
|
||||||
|
@ -182,43 +162,17 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
|
||||||
fullPath.append(URL_SEPARATOR)
|
fullPath.append(URL_SEPARATOR)
|
||||||
.append(optionService.getCategoriesPrefix())
|
.append(optionService.getCategoriesPrefix())
|
||||||
.append(URL_SEPARATOR)
|
.append(URL_SEPARATOR)
|
||||||
.append(child.getSlug())
|
.append(slug)
|
||||||
.append(optionService.getPathSuffix());
|
.append(optionService.getPathSuffix());
|
||||||
|
return fullPath.toString();
|
||||||
child.setFullPath(fullPath.toString());
|
|
||||||
|
|
||||||
if (!fillPassword) {
|
|
||||||
child.setPassword(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add child
|
|
||||||
parentCategory.getChildren().add(child);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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
|
@NonNull
|
||||||
private CategoryVO createTopLevelCategory() {
|
private CategoryVO convertToCategoryVo(Category category) {
|
||||||
CategoryVO topCategory = new CategoryVO();
|
Assert.notNull(category, "The category must not be null.");
|
||||||
// Set default value
|
CategoryVO categoryVo = new CategoryVO().convertFrom(category);
|
||||||
topCategory.setId(0);
|
categoryVo.setFullPath(buildCategoryFullPath(categoryVo.getSlug()));
|
||||||
topCategory.setChildren(new LinkedList<>());
|
return categoryVo;
|
||||||
topCategory.setParentId(-1);
|
|
||||||
|
|
||||||
return topCategory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -328,32 +282,79 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Category> listByParentId(Integer id) {
|
public List<Category> listByParentId(@NonNull Integer id) {
|
||||||
Assert.notNull(id, "Parent id must not be null");
|
Assert.notNull(id, "Parent id must not be null");
|
||||||
return categoryRepository.findByParentId(id);
|
return categoryRepository.findByParentId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Category> listAllByParentId(@NonNull Integer id) {
|
||||||
|
Assert.notNull(id, "Parent id must not be null");
|
||||||
|
List<Category> categories = super.listAll(Sort.by(Order.asc("name")));
|
||||||
|
List<CategoryVO> 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<Category> walkCategoryTree(CategoryVO root) {
|
||||||
|
Assert.notNull(root, "The category 'root' must not be null");
|
||||||
|
List<Category> categories = new LinkedList<>();
|
||||||
|
Queue<CategoryVO> 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 <code>categoryId</code> 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<CategoryVO> findCategoryTreeNodeById(List<CategoryVO> categoryVos,
|
||||||
|
Integer categoryId) {
|
||||||
|
Assert.notNull(categoryId, "categoryId id must not be null");
|
||||||
|
Queue<CategoryVO> 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
|
@Override
|
||||||
public CategoryDTO convertTo(Category category) {
|
public CategoryDTO convertTo(Category category) {
|
||||||
Assert.notNull(category, "Category must not be null");
|
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()) {
|
return categoryDto;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -373,19 +374,15 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Concrete the tree
|
// list to tree, no password desensitise is required here
|
||||||
CategoryVO topLevelCategory = createTopLevelCategory();
|
List<CategoryVO> categoryTree = listToTree(categories);
|
||||||
|
|
||||||
concreteTree(topLevelCategory, categories, true);
|
|
||||||
|
|
||||||
List<CategoryVO> childrenList = topLevelCategory.getChildren();
|
|
||||||
|
|
||||||
// filter encrypt category
|
// filter encrypt category
|
||||||
doFilterEncryptCategory(childrenList);
|
doFilterEncryptCategory(categoryTree);
|
||||||
|
|
||||||
List<Category> collectorList = new ArrayList<>();
|
List<Category> collectorList = new ArrayList<>();
|
||||||
|
|
||||||
collectAllChild(collectorList, childrenList, true);
|
collectAllChild(collectorList, categoryTree, true);
|
||||||
|
|
||||||
for (Category category : collectorList) {
|
for (Category category : collectorList) {
|
||||||
category.setPassword(null);
|
category.setPassword(null);
|
||||||
|
@ -551,14 +548,13 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
|
||||||
* @param category need encrypt category
|
* @param category need encrypt category
|
||||||
*/
|
*/
|
||||||
private void doEncryptPost(Category category) {
|
private void doEncryptPost(Category category) {
|
||||||
CategoryVO topLevelCategory = createTopLevelCategory();
|
// list to tree with password
|
||||||
|
List<CategoryVO> categoryTree = listToTree(super.listAll());
|
||||||
concreteTree(topLevelCategory, super.listAll(), true);
|
|
||||||
|
|
||||||
List<Category> collectorList = new ArrayList<>();
|
List<Category> collectorList = new ArrayList<>();
|
||||||
|
|
||||||
collectAllChildByCategoryId(collectorList,
|
collectAllChildByCategoryId(collectorList,
|
||||||
topLevelCategory.getChildren(), category.getId(), true);
|
categoryTree, category.getId(), true);
|
||||||
|
|
||||||
Optional.of(collectorList.stream().map(Category::getId).collect(Collectors.toList()))
|
Optional.of(collectorList.stream().map(Category::getId).collect(Collectors.toList()))
|
||||||
.map(categoryIdList -> {
|
.map(categoryIdList -> {
|
||||||
|
@ -594,16 +590,14 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
|
||||||
// If the parent category is encrypted, there is no need to update the encryption status
|
// If the parent category is encrypted, there is no need to update the encryption status
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// with password
|
||||||
CategoryVO topLevelCategory = createTopLevelCategory();
|
List<CategoryVO> categoryTree = listToTree(allCategoryList);
|
||||||
|
|
||||||
concreteTree(topLevelCategory, allCategoryList, true);
|
|
||||||
|
|
||||||
List<Category> collectorList = new ArrayList<>();
|
List<Category> collectorList = new ArrayList<>();
|
||||||
|
|
||||||
// Only collect unencrypted sub-categories under the category.
|
// Only collect unencrypted sub-categories under the category.
|
||||||
collectAllChildByCategoryId(collectorList,
|
collectAllChildByCategoryId(collectorList,
|
||||||
topLevelCategory.getChildren(), category.getId(), false);
|
categoryTree, category.getId(), false);
|
||||||
// Collect the currently decrypted category
|
// Collect the currently decrypted category
|
||||||
collectorList.add(category);
|
collectorList.add(category);
|
||||||
|
|
||||||
|
@ -637,6 +631,33 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
|
||||||
return doCategoryHasEncrypt(idToCategoryMap, categoryId);
|
return doCategoryHasEncrypt(idToCategoryMap, categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CategoryVO> listToTree(List<Category> categories) {
|
||||||
|
Assert.notNull(categories, "The categories must not be null.");
|
||||||
|
// batch convert category to categoryVo
|
||||||
|
List<CategoryVO> categoryVoList = categories.stream()
|
||||||
|
.map(this::convertToCategoryVo)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// build a tree, the time complexity is O(n)
|
||||||
|
Map<Integer, List<CategoryVO>> parentIdMap = categoryVoList.stream()
|
||||||
|
.collect(Collectors.groupingBy(CategoryVO::getParentId));
|
||||||
|
|
||||||
|
// set children
|
||||||
|
categoryVoList.forEach(category -> {
|
||||||
|
List<CategoryVO> 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.
|
* Find whether the parent category is encrypted.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
package run.halo.app.service.impl;
|
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.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Queue;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
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.Post;
|
||||||
import run.halo.app.model.entity.PostCategory;
|
import run.halo.app.model.entity.PostCategory;
|
||||||
import run.halo.app.model.enums.PostStatus;
|
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.PostCategoryRepository;
|
||||||
import run.halo.app.repository.PostRepository;
|
import run.halo.app.repository.PostRepository;
|
||||||
import run.halo.app.service.CategoryService;
|
import run.halo.app.service.CategoryService;
|
||||||
import run.halo.app.service.OptionService;
|
import run.halo.app.service.OptionService;
|
||||||
import run.halo.app.service.PostCategoryService;
|
import run.halo.app.service.PostCategoryService;
|
||||||
import run.halo.app.service.base.AbstractCrudService;
|
import run.halo.app.service.base.AbstractCrudService;
|
||||||
|
import run.halo.app.utils.HaloUtils;
|
||||||
import run.halo.app.utils.ServiceUtils;
|
import run.halo.app.utils.ServiceUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -304,44 +309,80 @@ public class PostCategoryServiceImpl extends AbstractCrudService<PostCategory, I
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<CategoryWithPostCountDTO> listCategoryWithPostCountDto(
|
public List<CategoryWithPostCountDTO> listCategoryWithPostCountDto(@NonNull Sort sort,
|
||||||
Sort sort, boolean queryEncryptCategory) {
|
boolean queryEncryptCategory) {
|
||||||
Assert.notNull(sort, "Sort info must not be null");
|
Assert.notNull(sort, "Sort info must not be null");
|
||||||
List<Category> categories = categoryService.listAll(sort, queryEncryptCategory);
|
List<Category> categories = categoryService.listAll(sort, queryEncryptCategory);
|
||||||
|
List<CategoryVO> categoryTreeVo = categoryService.listToTree(categories);
|
||||||
// Query category post count
|
populatePostIds(categoryTreeVo);
|
||||||
Map<Integer, Long> categoryPostCountMap = ServiceUtils
|
|
||||||
.convertToMap(postCategoryRepository.findPostCount(),
|
|
||||||
CategoryPostCountProjection::getCategoryId,
|
|
||||||
CategoryPostCountProjection::getPostCount);
|
|
||||||
|
|
||||||
// Convert and return
|
// Convert and return
|
||||||
return categories.stream()
|
return flatTreeToList(categoryTreeVo);
|
||||||
.map(category -> {
|
|
||||||
// Create category post count dto
|
|
||||||
CategoryWithPostCountDTO categoryWithPostCountDTO =
|
|
||||||
new CategoryWithPostCountDTO().convertFrom(category);
|
|
||||||
// Set post count
|
|
||||||
categoryWithPostCountDTO
|
|
||||||
.setPostCount(categoryPostCountMap.getOrDefault(category.getId(), 0L));
|
|
||||||
|
|
||||||
StringBuilder fullPath = new StringBuilder();
|
|
||||||
|
|
||||||
if (optionService.isEnabledAbsolutePath()) {
|
|
||||||
fullPath.append(optionService.getBlogBaseUrl());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fullPath.append(URL_SEPARATOR)
|
private List<CategoryWithPostCountDTO> flatTreeToList(List<CategoryVO> categoryTree) {
|
||||||
.append(optionService.getCategoriesPrefix())
|
Assert.notNull(categoryTree, "The categoryTree must not be null.");
|
||||||
.append(URL_SEPARATOR)
|
List<CategoryWithPostCountDTO> result = new LinkedList<>();
|
||||||
.append(category.getSlug())
|
walkCategoryTree(categoryTree, category -> {
|
||||||
.append(optionService.getPathSuffix());
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
categoryWithPostCountDTO.setFullPath(fullPath.toString());
|
private void populatePostIds(List<CategoryVO> categoryTree) {
|
||||||
|
Assert.notNull(categoryTree, "The categoryTree must not be null.");
|
||||||
|
Map<Integer, Set<Integer>> categoryPostIdsMap = postCategoryRepository.findAll()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.groupingBy(PostCategory::getCategoryId,
|
||||||
|
Collectors.mapping(PostCategory::getPostId, Collectors.toSet())));
|
||||||
|
|
||||||
return categoryWithPostCountDTO;
|
walkCategoryTree(categoryTree, category -> {
|
||||||
})
|
// Set post count
|
||||||
.collect(Collectors.toList());
|
Set<Integer> postIds =
|
||||||
|
categoryPostIdsMap.getOrDefault(category.getId(), new LinkedHashSet<>());
|
||||||
|
category.setPostIds(postIds);
|
||||||
|
});
|
||||||
|
CategoryVO categoryTreeRootNode = new CategoryVO();
|
||||||
|
categoryTreeRootNode.setChildren(categoryTree);
|
||||||
|
mergePostIdsFromBottomToTop(categoryTreeRootNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergePostIdsFromBottomToTop(CategoryVO root) {
|
||||||
|
if (root == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<CategoryVO> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void walkCategoryTree(List<CategoryVO> categoryTree, Consumer<CategoryVO> consumer) {
|
||||||
|
Queue<CategoryVO> queue = new ArrayDeque<>(categoryTree);
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
CategoryVO category = queue.poll();
|
||||||
|
consumer.accept(category);
|
||||||
|
if (HaloUtils.isNotEmpty(category.getChildren())) {
|
||||||
|
queue.addAll(category.getChildren());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -349,4 +390,5 @@ public class PostCategoryServiceImpl extends AbstractCrudService<PostCategory, I
|
||||||
Assert.notEmpty(categoryIdList, "category id list not empty");
|
Assert.notEmpty(categoryIdList, "category id list not empty");
|
||||||
return postCategoryRepository.findAllByCategoryIdList(categoryIdList);
|
return postCategoryRepository.findAllByCategoryIdList(categoryIdList);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -816,13 +816,17 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postQuery.getCategoryId() != null) {
|
if (postQuery.getCategoryId() != null) {
|
||||||
|
List<Integer> categoryIds =
|
||||||
|
categoryService.listAllByParentId(postQuery.getCategoryId())
|
||||||
|
.stream()
|
||||||
|
.map(Category::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
Subquery<Post> postSubquery = query.subquery(Post.class);
|
Subquery<Post> postSubquery = query.subquery(Post.class);
|
||||||
Root<PostCategory> postCategoryRoot = postSubquery.from(PostCategory.class);
|
Root<PostCategory> postCategoryRoot = postSubquery.from(PostCategory.class);
|
||||||
postSubquery.select(postCategoryRoot.get("postId"));
|
postSubquery.select(postCategoryRoot.get("postId"));
|
||||||
postSubquery.where(
|
postSubquery.where(
|
||||||
criteriaBuilder.equal(root.get("id"), postCategoryRoot.get("postId")),
|
criteriaBuilder.equal(root.get("id"), postCategoryRoot.get("postId")),
|
||||||
criteriaBuilder
|
postCategoryRoot.get("categoryId").in(categoryIds));
|
||||||
.equal(postCategoryRoot.get("categoryId"), postQuery.getCategoryId()));
|
|
||||||
predicates.add(criteriaBuilder.exists(postSubquery));
|
predicates.add(criteriaBuilder.exists(postSubquery));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<Category> categories = mockCategories();
|
||||||
|
List<CategoryVO> 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<Category> 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue