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;
+ }
}