From d14b46d007ede6573ace30295811817e0de767c3 Mon Sep 17 00:00:00 2001 From: huangpeng <1764183241@qq.com> Date: Thu, 11 Sep 2025 22:33:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/constants/CommonConstant.java | 13 + .../common/enums/CommonStateEnum.java | 63 +++ .../interview/common/utils/TreeUtil.java | 59 +++ .../QuestionCategoryController.java | 15 + .../interview/dto/QuestionCategoryDTO.java | 34 ++ .../dto/QuestionCategoryPageParams.java | 47 +++ .../interview/entity/QuestionCategory.java | 53 ++- .../service/IQuestionCategoryService.java | 90 +++++ .../impl/QuestionCategoryServiceImpl.java | 369 +++++++++++++++++- 9 files changed, 738 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/qingqiu/interview/common/constants/CommonConstant.java create mode 100644 src/main/java/com/qingqiu/interview/common/enums/CommonStateEnum.java create mode 100644 src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java create mode 100644 src/main/java/com/qingqiu/interview/controller/QuestionCategoryController.java create mode 100644 src/main/java/com/qingqiu/interview/dto/QuestionCategoryDTO.java create mode 100644 src/main/java/com/qingqiu/interview/dto/QuestionCategoryPageParams.java diff --git a/src/main/java/com/qingqiu/interview/common/constants/CommonConstant.java b/src/main/java/com/qingqiu/interview/common/constants/CommonConstant.java new file mode 100644 index 0000000..5fc459b --- /dev/null +++ b/src/main/java/com/qingqiu/interview/common/constants/CommonConstant.java @@ -0,0 +1,13 @@ +package com.qingqiu.interview.common.constants; + +/** + *

公共常量

+ * @author huangpeng + * @date 2025/9/11 09:30 + */ +public class CommonConstant { + + public static final Integer ZERO = 0; + public static final Integer ONE = 1; + public static final Long ROOT_PARENT_ID = 0L; +} diff --git a/src/main/java/com/qingqiu/interview/common/enums/CommonStateEnum.java b/src/main/java/com/qingqiu/interview/common/enums/CommonStateEnum.java new file mode 100644 index 0000000..7951f41 --- /dev/null +++ b/src/main/java/com/qingqiu/interview/common/enums/CommonStateEnum.java @@ -0,0 +1,63 @@ +package com.qingqiu.interview.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + *

+ * + * @author huangpeng + * @date 2025/9/11 09:49 + */ +@Getter +@AllArgsConstructor +public enum CommonStateEnum { + /** + * 禁用状态 + */ + DISABLED(0, "禁用"), + + /** + * 启用状态 + */ + ENABLED(1, "启用"), + ; + + /** + * 状态码 + */ + private final Integer code; + + private final String value; + + /** + * 根据状态码获取枚举 + */ + public static CommonStateEnum getByCode(Integer code) { + if (code == null) { + return null; + } + for (CommonStateEnum state : values()) { + if (state.getCode().equals(code)) { + return state; + } + } + return null; + } + + /** + * 根据标识获取枚举 + */ + public static CommonStateEnum getByValue(String value) { + if (value == null || value.isEmpty()) { + return null; + } + for (CommonStateEnum state : values()) { + if (state.getValue().equalsIgnoreCase(value)) { + return state; + } + } + return null; + } +} diff --git a/src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java b/src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java new file mode 100644 index 0000000..faa48fd --- /dev/null +++ b/src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java @@ -0,0 +1,59 @@ +package com.qingqiu.interview.common.utils; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class TreeUtil { + + /** + * 通用树形结构构建方法 + */ + public static List buildTree(List list, + Function idGetter, + Function parentIdGetter, + Function> childrenSetter, + ID rootParentId) { + if (list == null || list.isEmpty()) { + return Collections.emptyList(); + } + + // 按父ID分组 + Map> parentMap = list.stream() + .collect(Collectors.groupingBy(parentIdGetter)); + + // 设置子节点 + list.forEach(item -> { + List children = parentMap.get(idGetter.apply(item)); + if (children != null && !children.isEmpty()) { + childrenSetter.apply(item).addAll(children); + } + }); + + // 返回根节点 + return parentMap.get(rootParentId); + } + + /** + * 扁平化树形结构 + */ + public static List flattenTree(List tree, Function> childrenGetter) { + List result = new ArrayList<>(); + flattenTreeRecursive(tree, childrenGetter, result); + return result; + } + + private static void flattenTreeRecursive(List nodes, + Function> childrenGetter, + List result) { + if (nodes == null) return; + + for (T node : nodes) { + result.add(node); + List children = childrenGetter.apply(node); + if (children != null && !children.isEmpty()) { + flattenTreeRecursive(children, childrenGetter, result); + } + } + } +} diff --git a/src/main/java/com/qingqiu/interview/controller/QuestionCategoryController.java b/src/main/java/com/qingqiu/interview/controller/QuestionCategoryController.java new file mode 100644 index 0000000..ca46d2d --- /dev/null +++ b/src/main/java/com/qingqiu/interview/controller/QuestionCategoryController.java @@ -0,0 +1,15 @@ +package com.qingqiu.interview.controller; + + +import com.qingqiu.interview.service.IQuestionCategoryService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/question-category") +@RequiredArgsConstructor +public class QuestionCategoryController { + + private final IQuestionCategoryService questionCategoryService; +} diff --git a/src/main/java/com/qingqiu/interview/dto/QuestionCategoryDTO.java b/src/main/java/com/qingqiu/interview/dto/QuestionCategoryDTO.java new file mode 100644 index 0000000..3412196 --- /dev/null +++ b/src/main/java/com/qingqiu/interview/dto/QuestionCategoryDTO.java @@ -0,0 +1,34 @@ +package com.qingqiu.interview.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + *

+ * + * @author huangpeng + * @date 2025/9/11 09:39 + */ +@Data +public class QuestionCategoryDTO { + + private Long id; + + @NotBlank(message = "分类名称不能为空") + private String name; + + @NotNull(message = "父级分类ID不能为空") + private Long parentId; + + @NotNull(message = "排序不能为空") + private Integer sort; + + @NotNull(message = "状态不能为空") + private Integer state; + + /** + * 父分类名称(用于前端显示) + */ + private String parentName; +} diff --git a/src/main/java/com/qingqiu/interview/dto/QuestionCategoryPageParams.java b/src/main/java/com/qingqiu/interview/dto/QuestionCategoryPageParams.java new file mode 100644 index 0000000..9cc76d1 --- /dev/null +++ b/src/main/java/com/qingqiu/interview/dto/QuestionCategoryPageParams.java @@ -0,0 +1,47 @@ +package com.qingqiu.interview.dto; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * + * @author huangpeng + * @date 2025/9/11 09:40 + */ + +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +public class QuestionCategoryPageParams extends PageBaseParams{ + /** + * 分类名称(模糊查询) + */ + private String name; + + /** + * 状态(0:禁用,1:启用) + */ + private Integer state; + + /** + * 父级分类ID + */ + private Long parentId; + + /** + * 层级 + */ + private Integer level; + + /** + * 是否包含子分类 + */ + private Boolean includeChildren = false; + + /** + * 是否只返回启用状态的分类 + */ + private Boolean onlyEnabled = false; +} diff --git a/src/main/java/com/qingqiu/interview/entity/QuestionCategory.java b/src/main/java/com/qingqiu/interview/entity/QuestionCategory.java index f0cfcb9..4dcf849 100644 --- a/src/main/java/com/qingqiu/interview/entity/QuestionCategory.java +++ b/src/main/java/com/qingqiu/interview/entity/QuestionCategory.java @@ -1,10 +1,11 @@ package com.qingqiu.interview.entity; -import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.*; + import java.time.LocalDateTime; import java.io.Serializable; +import java.util.List; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; @@ -33,16 +34,60 @@ public class QuestionCategory implements Serializable { */ private String name; + /** + * 上级id + */ + private Long parentId; + + /** + * 层级 + */ + private Integer level; + + /** + * 上级序列 + */ + private String ancestor; + /** * 排序 */ private Integer sort; + /** + * 状态 0 禁用 1 启用 + */ + private Integer state; + + @TableField(fill = FieldFill.INSERT) private LocalDateTime createdTime; + @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedTime; - + @TableLogic private Integer deleted; + /** + * 子分类列表(非数据库字段) + */ + @TableField(exist = false) + private List children; + + /** + * 子分类数量(非数据库字段) + */ + @TableField(exist = false) + private Integer childrenCount; + /** + * 父分类名称(非数据库字段,用于显示) + */ + @TableField(exist = false) + private String parentName; + + + + + + } diff --git a/src/main/java/com/qingqiu/interview/service/IQuestionCategoryService.java b/src/main/java/com/qingqiu/interview/service/IQuestionCategoryService.java index 3ae0a68..16a5f42 100644 --- a/src/main/java/com/qingqiu/interview/service/IQuestionCategoryService.java +++ b/src/main/java/com/qingqiu/interview/service/IQuestionCategoryService.java @@ -1,8 +1,15 @@ package com.qingqiu.interview.service; +import cn.hutool.db.PageResult; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.qingqiu.interview.dto.QuestionCategoryDTO; +import com.qingqiu.interview.dto.QuestionCategoryPageParams; import com.qingqiu.interview.entity.QuestionCategory; import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; + /** *

* 题型分类 服务类 @@ -12,5 +19,88 @@ import com.baomidou.mybatisplus.extension.service.IService; * @since 2025-09-08 */ public interface IQuestionCategoryService extends IService { + /** + * 获取分类树列表 + */ + List getTreeList(); + /** + * 获取分类选项(用于下拉选择) + */ + List getOptions(); + + /** + * 创建分类 + */ + Long createCategory(QuestionCategoryDTO dto); + + /** + * 更新分类 + */ + void updateCategory(Long id, QuestionCategoryDTO dto); + + /** + * 删除分类 + */ + void deleteCategory(Long id); + + /** + * 更新分类状态 + */ + void updateState(Long id, Integer state); + + /** + * 获取分类详情 + */ + QuestionCategory getCategoryDetail(Long id); + + /** + * 分页查询分类 + */ + Page getCategoryPage(QuestionCategoryPageParams query); + + /** + * 根据名称搜索分类 + */ + List searchByName(String name); + + /** + * 获取某个分类的所有子孙分类 + */ + List getAllDescendants(Long parentId); + + /** + * 批量更新分类状态(包含子孙分类) + */ + void batchUpdateState(Long parentId, Integer state); + + /** + * 获取分类的完整路径名称 + */ + String getFullPathName(Long categoryId); + + /** + * 检查分类名称是否重复 + */ + boolean checkNameExists(String name, Long parentId, Long excludeId); + + /** + * 移动分类(修改父分类) + */ + void moveCategory(Long id, Long newParentId); + + /** + * 获取指定层级的分类 + */ + List getCategoriesByLevel(Integer level); + + /** + * 获取启用的分类树 + */ + List getEnabledTreeList(); + + /** + * 根据父ID获取子分类 + */ + List getChildrenByParentId(Long parentId); } diff --git a/src/main/java/com/qingqiu/interview/service/impl/QuestionCategoryServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/QuestionCategoryServiceImpl.java index cd5fcf0..22b7468 100644 --- a/src/main/java/com/qingqiu/interview/service/impl/QuestionCategoryServiceImpl.java +++ b/src/main/java/com/qingqiu/interview/service/impl/QuestionCategoryServiceImpl.java @@ -1,10 +1,26 @@ package com.qingqiu.interview.service.impl; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.db.PageResult; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.qingqiu.interview.common.constants.CommonConstant; +import com.qingqiu.interview.common.enums.CommonStateEnum; +import com.qingqiu.interview.dto.QuestionCategoryDTO; +import com.qingqiu.interview.dto.QuestionCategoryPageParams; import com.qingqiu.interview.entity.QuestionCategory; import com.qingqiu.interview.mapper.QuestionCategoryMapper; import com.qingqiu.interview.service.IQuestionCategoryService; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; /** *

@@ -14,7 +30,358 @@ import org.springframework.stereotype.Service; * @author huangpeng * @since 2025-09-08 */ +@Slf4j @Service public class QuestionCategoryServiceImpl extends ServiceImpl implements IQuestionCategoryService { + @Override + public List getTreeList() { + List allCategories = getAllValidCategories(); + return buildCategoryTree(allCategories); + } + @Override + public List getOptions() { + List allCategories = getAllValidCategories(); + + return allCategories.stream() + .filter(category -> CommonStateEnum.ENABLED.getCode().equals(category.getState())) + .sorted(Comparator.comparingInt(QuestionCategory::getSort)) + .collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createCategory(QuestionCategoryDTO dto) { + // 检查名称是否重复 + if (checkNameExists(dto.getName(), dto.getParentId(), null)) { + throw new RuntimeException("同一层级下分类名称不能重复"); + } + + validateParentCategory(dto.getParentId()); + + QuestionCategory category = new QuestionCategory(); + BeanUtils.copyProperties(dto, category); + + calculateLevelAndPath(category, dto.getParentId()); + + // 保存分类 + save(category); + + // 更新路径(需要ID) + updateCategoryPathAfterSave(category, dto.getParentId()); + + log.info("创建分类成功:{}", category); + return category.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCategory(Long id, QuestionCategoryDTO dto) { + QuestionCategory category = getById(id); + if (category == null) { + throw new RuntimeException("分类不存在"); + } + + // 检查名称是否重复(排除自身) + if (checkNameExists(dto.getName(), category.getParentId(), id)) { + throw new RuntimeException("同一层级下分类名称不能重复"); + } + + // 检查是否修改了父分类 + if (!category.getParentId().equals(dto.getParentId())) { + throw new RuntimeException("不支持直接修改父分类,请使用移动分类功能"); + } + + BeanUtils.copyProperties(dto, category); + category.setUpdatedTime(LocalDateTime.now()); + updateById(category); + + log.info("更新分类成功:{}", category); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteCategory(Long id) { + QuestionCategory category = getById(id); + if (category == null) { + throw new RuntimeException("分类不存在"); + } + + checkChildrenExists(id); + removeById(id); + + log.info("删除分类成功:{}", id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateState(Long id, Integer state) { + QuestionCategory category = new QuestionCategory(); + category.setId(id); + category.setState(state); + category.setUpdatedTime(LocalDateTime.now()); + updateById(category); + + log.info("更新分类状态成功:id={}, state={}", id, state); + } + + @Override + public QuestionCategory getCategoryDetail(Long id) { + QuestionCategory category = getById(id); + if (category != null && !CommonConstant.ONE.equals(category.getDeleted())) { + // 设置父分类名称 + if (!CommonConstant.ROOT_PARENT_ID.equals(category.getParentId())) { + QuestionCategory parent = getById(category.getParentId()); + if (parent != null) { + category.setParentName(parent.getName()); + } + } + return category; + } + return null; + } + + @Override + public Page getCategoryPage(QuestionCategoryPageParams query) { + return page( + Page.of(query.getCurrent(), query.getSize()), + new LambdaQueryWrapper() + .like(StringUtils.hasText(query.getName()), QuestionCategory::getName, query.getName()) + .eq(QuestionCategory::getState, query.getState()) + .or(Objects.nonNull(query.getParentId()), wrapper -> { + wrapper.eq(QuestionCategory::getParentId, query.getParentId()) + .or() + .apply("find_in_set({0}, ancestor)", query.getParentId()) + ; + }) + .orderByDesc(QuestionCategory::getSort) + .orderByDesc(QuestionCategory::getCreatedTime) + ); + } + + @Override + public List searchByName(String name) { + if (!StringUtils.hasText(name)) { + return Collections.emptyList(); + } + + List allCategories = getAllValidCategories(); + + return allCategories.stream() + .filter(category -> category.getName().toLowerCase().contains(name.toLowerCase())) + .sorted(Comparator.comparingInt(QuestionCategory::getSort)) + .collect(Collectors.toList()); + } + + @Override + public List getAllDescendants(Long parentId) { + List allCategories = getAllValidCategories(); + return findDescendants(allCategories, parentId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchUpdateState(Long parentId, Integer state) { + List descendants = getAllDescendants(parentId); + descendants.forEach(category -> { + category.setState(state); + category.setUpdatedTime(LocalDateTime.now()); + }); + + updateBatchById(descendants); + log.info("批量更新分类状态成功:parentId={}, state={}, count={}", parentId, state, descendants.size()); + } + + @Override + public String getFullPathName(Long categoryId) { + List allCategories = getAllValidCategories(); + Map categoryMap = allCategories.stream() + .collect(Collectors.toMap(QuestionCategory::getId, category -> category)); + + List pathNames = new ArrayList<>(); + QuestionCategory current = categoryMap.get(categoryId); + + while (current != null) { + pathNames.add(current.getName()); + current = categoryMap.get(current.getParentId()); + } + + Collections.reverse(pathNames); + return String.join("/", pathNames); + } + + @Override + public boolean checkNameExists(String name, Long parentId, Long excludeId) { + List allCategories = getAllValidCategories(); + + return allCategories.stream() + .filter(category -> category.getName().equals(name)) + .filter(category -> parentId.equals(category.getParentId())) + .anyMatch(category -> excludeId == null || !excludeId.equals(category.getId())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void moveCategory(Long id, Long newParentId) { + QuestionCategory category = getById(id); + if (category == null) { + throw new RuntimeException("分类不存在"); + } + + if (category.getParentId().equals(newParentId)) { + throw new RuntimeException("新父分类与当前父分类相同"); + } + + validateParentCategory(newParentId); + + // 检查名称是否重复 + if (checkNameExists(category.getName(), newParentId, id)) { + throw new RuntimeException("目标父分类下已存在相同名称的分类"); + } + + // 更新父分类 + category.setParentId(newParentId); + + // 重新计算层级和路径 + QuestionCategory newParent = getById(newParentId); + if (CommonConstant.ROOT_PARENT_ID.equals(newParentId)) { + category.setLevel(1); + category.setAncestor(String.valueOf(category.getId())); + } else { + category.setLevel(newParent.getLevel() + 1); + category.setAncestor(newParent.getAncestor() + "," + category.getId()); + } + + category.setUpdatedTime(LocalDateTime.now()); + updateById(category); + + log.info("移动分类成功:id={}, newParentId={}", id, newParentId); + } + + @Override + public List getCategoriesByLevel(Integer level) { + List allCategories = getAllValidCategories(); + + return allCategories.stream() + .filter(category -> level.equals(category.getLevel())) + .sorted(Comparator.comparingInt(QuestionCategory::getSort)) + .collect(Collectors.toList()); + } + + @Override + public List getEnabledTreeList() { + List allCategories = getAllValidCategories(); + + List enabledCategories = allCategories.stream() + .filter(category -> CommonStateEnum.ENABLED.getCode().equals(category.getState())) + .collect(Collectors.toList()); + + return buildCategoryTree(enabledCategories); + } + + @Override + public List getChildrenByParentId(Long parentId) { + List allCategories = getAllValidCategories(); + + return allCategories.stream() + .filter(category -> parentId.equals(category.getParentId())) + .sorted(Comparator.comparingInt(QuestionCategory::getSort)) + .collect(Collectors.toList()); + } + + // ============ 私有方法 ============ + + private List getAllValidCategories() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QuestionCategory::getDeleted, CommonConstant.ZERO) + .orderByAsc(QuestionCategory::getSort); + return list(wrapper); + } + + private List buildCategoryTree(List categories) { + if (CollectionUtil.isEmpty(categories)) { + return Collections.emptyList(); + } + + // 按父ID分组 + Map> parentIdMap = categories.stream() + .collect(Collectors.groupingBy(QuestionCategory::getParentId)); + + // 设置子节点并计算子节点数量 + categories.forEach(category -> { + List children = parentIdMap.get(category.getId()); + if (!CollectionUtil.isEmpty(children)) { + category.setChildren(children); + category.setChildrenCount(children.size()); + children.sort(Comparator.comparingInt(QuestionCategory::getSort)); + } else { + category.setChildrenCount(0); + } + }); + + // 返回根节点 + return parentIdMap.getOrDefault(CommonConstant.ROOT_PARENT_ID, Collections.emptyList()); + } + + private void validateParentCategory(Long parentId) { + if (!CommonConstant.ROOT_PARENT_ID.equals(parentId)) { + QuestionCategory parentCategory = getById(parentId); + if (parentCategory == null || CommonConstant.ONE.equals(parentCategory.getDeleted())) { + throw new RuntimeException("父分类不存在或已被删除"); + } + if (CommonStateEnum.DISABLED.getCode().equals(parentCategory.getState())) { + throw new RuntimeException("父分类已被禁用,无法创建子分类"); + } + } + } + + private void calculateLevelAndPath(QuestionCategory category, Long parentId) { + if (CommonConstant.ROOT_PARENT_ID.equals(parentId)) { + category.setLevel(1); + } else { + QuestionCategory parentCategory = getById(parentId); + category.setLevel(parentCategory.getLevel() + 1); + + if (category.getLevel() > 5) { + throw new RuntimeException("分类层级过深,最多支持5级分类"); + } + } + } + + @Transactional(rollbackFor = Exception.class) + public void updateCategoryPathAfterSave(QuestionCategory category, Long parentId) { + if (!CommonConstant.ROOT_PARENT_ID.equals(parentId)) { + QuestionCategory parentCategory = getById(parentId); + String newPath = parentCategory.getAncestor() + "," + category.getId(); + category.setAncestor(newPath); + updateById(category); + } else { + category.setAncestor(String.valueOf(category.getId())); + updateById(category); + } + } + + private void checkChildrenExists(Long parentId) { + List allCategories = getAllValidCategories(); + boolean hasChildren = allCategories.stream() + .anyMatch(category -> parentId.equals(category.getParentId())); + + if (hasChildren) { + throw new RuntimeException("存在子分类,无法删除"); + } + } + + + private List findDescendants(List allCategories, Long parentId) { + List descendants = new ArrayList<>(); + + allCategories.stream() + .filter(category -> parentId.equals(category.getParentId())) + .forEach(category -> { + descendants.add(category); + descendants.addAll(findDescendants(allCategories, category.getId())); + }); + + return descendants; + } }