修改代码
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
package com.qingqiu.interview.common.constants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>公共常量</h1>
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.qingqiu.interview.common.enums;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <T, ID> List<T> buildTree(List<T> list,
|
||||||
|
Function<T, ID> idGetter,
|
||||||
|
Function<T, ID> parentIdGetter,
|
||||||
|
Function<T, List<T>> childrenSetter,
|
||||||
|
ID rootParentId) {
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按父ID分组
|
||||||
|
Map<ID, List<T>> parentMap = list.stream()
|
||||||
|
.collect(Collectors.groupingBy(parentIdGetter));
|
||||||
|
|
||||||
|
// 设置子节点
|
||||||
|
list.forEach(item -> {
|
||||||
|
List<T> children = parentMap.get(idGetter.apply(item));
|
||||||
|
if (children != null && !children.isEmpty()) {
|
||||||
|
childrenSetter.apply(item).addAll(children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回根节点
|
||||||
|
return parentMap.get(rootParentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扁平化树形结构
|
||||||
|
*/
|
||||||
|
public static <T> List<T> flattenTree(List<T> tree, Function<T, List<T>> childrenGetter) {
|
||||||
|
List<T> result = new ArrayList<>();
|
||||||
|
flattenTreeRecursive(tree, childrenGetter, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> void flattenTreeRecursive(List<T> nodes,
|
||||||
|
Function<T, List<T>> childrenGetter,
|
||||||
|
List<T> result) {
|
||||||
|
if (nodes == null) return;
|
||||||
|
|
||||||
|
for (T node : nodes) {
|
||||||
|
result.add(node);
|
||||||
|
List<T> children = childrenGetter.apply(node);
|
||||||
|
if (children != null && !children.isEmpty()) {
|
||||||
|
flattenTreeRecursive(children, childrenGetter, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.qingqiu.interview.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.qingqiu.interview.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.qingqiu.interview.entity;
|
package com.qingqiu.interview.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
@@ -33,16 +34,60 @@ public class QuestionCategory implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上级id
|
||||||
|
*/
|
||||||
|
private Long parentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 层级
|
||||||
|
*/
|
||||||
|
private Integer level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上级序列
|
||||||
|
*/
|
||||||
|
private String ancestor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 排序
|
* 排序
|
||||||
*/
|
*/
|
||||||
private Integer sort;
|
private Integer sort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态 0 禁用 1 启用
|
||||||
|
*/
|
||||||
|
private Integer state;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
private LocalDateTime createdTime;
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
private LocalDateTime updatedTime;
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
private Integer deleted;
|
private Integer deleted;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子分类列表(非数据库字段)
|
||||||
|
*/
|
||||||
|
@TableField(exist = false)
|
||||||
|
private List<QuestionCategory> children;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子分类数量(非数据库字段)
|
||||||
|
*/
|
||||||
|
@TableField(exist = false)
|
||||||
|
private Integer childrenCount;
|
||||||
|
/**
|
||||||
|
* 父分类名称(非数据库字段,用于显示)
|
||||||
|
*/
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String parentName;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package com.qingqiu.interview.service;
|
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.qingqiu.interview.entity.QuestionCategory;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* 题型分类 服务类
|
* 题型分类 服务类
|
||||||
@@ -12,5 +19,88 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||||||
* @since 2025-09-08
|
* @since 2025-09-08
|
||||||
*/
|
*/
|
||||||
public interface IQuestionCategoryService extends IService<QuestionCategory> {
|
public interface IQuestionCategoryService extends IService<QuestionCategory> {
|
||||||
|
/**
|
||||||
|
* 获取分类树列表
|
||||||
|
*/
|
||||||
|
List<QuestionCategory> getTreeList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分类选项(用于下拉选择)
|
||||||
|
*/
|
||||||
|
List<QuestionCategory> 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<QuestionCategory> getCategoryPage(QuestionCategoryPageParams query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据名称搜索分类
|
||||||
|
*/
|
||||||
|
List<QuestionCategory> searchByName(String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取某个分类的所有子孙分类
|
||||||
|
*/
|
||||||
|
List<QuestionCategory> 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<QuestionCategory> getCategoriesByLevel(Integer level);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取启用的分类树
|
||||||
|
*/
|
||||||
|
List<QuestionCategory> getEnabledTreeList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据父ID获取子分类
|
||||||
|
*/
|
||||||
|
List<QuestionCategory> getChildrenByParentId(Long parentId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
package com.qingqiu.interview.service.impl;
|
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.entity.QuestionCategory;
|
||||||
import com.qingqiu.interview.mapper.QuestionCategoryMapper;
|
import com.qingqiu.interview.mapper.QuestionCategoryMapper;
|
||||||
import com.qingqiu.interview.service.IQuestionCategoryService;
|
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.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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -14,7 +30,358 @@ import org.springframework.stereotype.Service;
|
|||||||
* @author huangpeng
|
* @author huangpeng
|
||||||
* @since 2025-09-08
|
* @since 2025-09-08
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class QuestionCategoryServiceImpl extends ServiceImpl<QuestionCategoryMapper, QuestionCategory> implements IQuestionCategoryService {
|
public class QuestionCategoryServiceImpl extends ServiceImpl<QuestionCategoryMapper, QuestionCategory> implements IQuestionCategoryService {
|
||||||
|
@Override
|
||||||
|
public List<QuestionCategory> getTreeList() {
|
||||||
|
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||||
|
return buildCategoryTree(allCategories);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<QuestionCategory> getOptions() {
|
||||||
|
List<QuestionCategory> 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<QuestionCategory> getCategoryPage(QuestionCategoryPageParams query) {
|
||||||
|
return page(
|
||||||
|
Page.of(query.getCurrent(), query.getSize()),
|
||||||
|
new LambdaQueryWrapper<QuestionCategory>()
|
||||||
|
.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<QuestionCategory> searchByName(String name) {
|
||||||
|
if (!StringUtils.hasText(name)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||||
|
|
||||||
|
return allCategories.stream()
|
||||||
|
.filter(category -> category.getName().toLowerCase().contains(name.toLowerCase()))
|
||||||
|
.sorted(Comparator.comparingInt(QuestionCategory::getSort))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<QuestionCategory> getAllDescendants(Long parentId) {
|
||||||
|
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||||
|
return findDescendants(allCategories, parentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void batchUpdateState(Long parentId, Integer state) {
|
||||||
|
List<QuestionCategory> 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<QuestionCategory> allCategories = getAllValidCategories();
|
||||||
|
Map<Long, QuestionCategory> categoryMap = allCategories.stream()
|
||||||
|
.collect(Collectors.toMap(QuestionCategory::getId, category -> category));
|
||||||
|
|
||||||
|
List<String> 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<QuestionCategory> 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<QuestionCategory> getCategoriesByLevel(Integer level) {
|
||||||
|
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||||
|
|
||||||
|
return allCategories.stream()
|
||||||
|
.filter(category -> level.equals(category.getLevel()))
|
||||||
|
.sorted(Comparator.comparingInt(QuestionCategory::getSort))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<QuestionCategory> getEnabledTreeList() {
|
||||||
|
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||||
|
|
||||||
|
List<QuestionCategory> enabledCategories = allCategories.stream()
|
||||||
|
.filter(category -> CommonStateEnum.ENABLED.getCode().equals(category.getState()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return buildCategoryTree(enabledCategories);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<QuestionCategory> getChildrenByParentId(Long parentId) {
|
||||||
|
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||||
|
|
||||||
|
return allCategories.stream()
|
||||||
|
.filter(category -> parentId.equals(category.getParentId()))
|
||||||
|
.sorted(Comparator.comparingInt(QuestionCategory::getSort))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 私有方法 ============
|
||||||
|
|
||||||
|
private List<QuestionCategory> getAllValidCategories() {
|
||||||
|
LambdaQueryWrapper<QuestionCategory> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(QuestionCategory::getDeleted, CommonConstant.ZERO)
|
||||||
|
.orderByAsc(QuestionCategory::getSort);
|
||||||
|
return list(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QuestionCategory> buildCategoryTree(List<QuestionCategory> categories) {
|
||||||
|
if (CollectionUtil.isEmpty(categories)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按父ID分组
|
||||||
|
Map<Long, List<QuestionCategory>> parentIdMap = categories.stream()
|
||||||
|
.collect(Collectors.groupingBy(QuestionCategory::getParentId));
|
||||||
|
|
||||||
|
// 设置子节点并计算子节点数量
|
||||||
|
categories.forEach(category -> {
|
||||||
|
List<QuestionCategory> 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<QuestionCategory> allCategories = getAllValidCategories();
|
||||||
|
boolean hasChildren = allCategories.stream()
|
||||||
|
.anyMatch(category -> parentId.equals(category.getParentId()));
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
throw new RuntimeException("存在子分类,无法删除");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<QuestionCategory> findDescendants(List<QuestionCategory> allCategories, Long parentId) {
|
||||||
|
List<QuestionCategory> descendants = new ArrayList<>();
|
||||||
|
|
||||||
|
allCategories.stream()
|
||||||
|
.filter(category -> parentId.equals(category.getParentId()))
|
||||||
|
.forEach(category -> {
|
||||||
|
descendants.add(category);
|
||||||
|
descendants.addAll(findDescendants(allCategories, category.getId()));
|
||||||
|
});
|
||||||
|
|
||||||
|
return descendants;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user