修改代码

This commit is contained in:
2025-09-11 22:33:53 +08:00
parent 704ea4ab7b
commit d14b46d007
9 changed files with 738 additions and 5 deletions

View File

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

View File

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

View File

@@ -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);
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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<QuestionCategory> children;
/**
* 子分类数量(非数据库字段)
*/
@TableField(exist = false)
private Integer childrenCount;
/**
* 父分类名称(非数据库字段,用于显示)
*/
@TableField(exist = false)
private String parentName;
}

View File

@@ -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;
/**
* <p>
* 题型分类 服务类
@@ -12,5 +19,88 @@ import com.baomidou.mybatisplus.extension.service.IService;
* @since 2025-09-08
*/
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);
}

View File

@@ -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;
/**
* <p>
@@ -14,7 +30,358 @@ import org.springframework.stereotype.Service;
* @author huangpeng
* @since 2025-09-08
*/
@Slf4j
@Service
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;
}
}