如何实现多级分类,方便维护,容易获取子分类、父级分类、某个子分类的顶级分类的所有子分类;
一、数据建表
DROp TABLE IF EXISTS `oa_classification`; CREATE TABLE `oa_classification` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类id', `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '分类名称', `parent_id` bigint(20) NULL DEFAULT 0 COMMENT '父分类id', `sort` int(11) NOT NULL DEFAULT 0 COMMENT '显示顺序', `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '分类状态(0正常 1停用)', `deth` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '纵深', `deth_name` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '纵深名称', `deth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '纵深ID', `create_by` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '项目分类表' ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;二、maven主要依赖
三、代码 1、controllermysql mysql-connector-javacom.baomidou mybatis-plus-boot-starter3.5.1 cn.hutool hutool-all5.8.0
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
@Api(tags = "OA-系统设置-报销项目分类")
@RestController
@RequestMapping("/projectManagement")
@Validated
@Slf4j
public class ClassificationController extends BaseController {
@Resource
private OaClassificationService classificationService;
@PostMapping("/classification/create")
@ApiOperation("创建报销项目分类")
public AjaxResult createClassification(@Valid @RequestBody ClassificationCreateReqVO reqVO) {
Long classificationId = classificationService.createClassification(reqVO);
return toAjax(true);
}
@PutMapping("update")
@ApiOperation("更新报销项目分类")
public AjaxResult updateClassification(@Valid @RequestBody ClassificationUpdateReqVO reqVO) {
classificationService.updateClassification(reqVO);
return toAjax(true);
}
@GetMapping("/get")
@ApiOperation("获得报销项目分类信息")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
public AjaxResult getClassification(@RequestParam("id") Long id) {
OaClassification classification = classificationService.getClassification(id);
return success(classification);
}
@DeleteMapping("delete")
@ApiOperation("删除报销项目分类")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1", dataTypeClass = Long.class)
public AjaxResult deleteClassification(@RequestParam("id") Long id) {
classificationService.deleteClassification(id);
return toAjax(true);
}
@GetMapping("/listChildren")
@ApiOperation("获取报销项目分类列表-子分类")
public AjaxResult listClassificationsChildren(ClassificationListReqVO reqVO) {
List list = classificationService.getSimpleClassifications(reqVO);
//配置
TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
// 自定义属性名 都要默认值的
// 排序字段,这个字段不能是null,不然会报错,默认最好是数字
treeNodeConfig.setWeightKey("sort");
treeNodeConfig.setIdKey("id");
// 父级id字段
treeNodeConfig.setParentIdKey("parentId");
// 最大递归深度
//treeNodeConfig.setDeep(3);
//转换器
List> build = TreeUtil.build(list, 0, treeNodeConfig, ((object, treeNode) -> {
treeNode.setId(object.getId().intValue());//id
treeNode.setParentId(object.getParentId().intValue());//父id
treeNode.putExtra("name", object.getName());
treeNode.putExtra("sort", object.getSort());
treeNode.putExtra("status", object.getStatus());
}));
log.info(JSON.toJSONString(build));
return success(build);
}
@GetMapping("/list-all-simple-children")
@ApiOperation(value = "获取报销项目分类精简信息列表-子分类", notes = "只包含被开启的报销项目分类,主要用于前端的下拉选项")
public AjaxResult getSimpleClassificationsChildren() {
List> simpleClassificationsChildren = classificationService.getSimpleClassificationsChildren();
return success(simpleClassificationsChildren);
}
}
2、service
import cn.hutool.core.lang.tree.Tree; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; public interface OaClassificationService extends IService{ Long createClassification(ClassificationCreateReqVO reqVO); void updateClassification(ClassificationUpdateReqVO reqVO); void deleteClassification(Long id); List getSimpleClassifications(ClassificationListReqVO reqVO); List > getSimpleClassificationsChildren(); void getClassiByParentId( List list,Long parentId); OaClassification getClassification(Long id); }
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.tree.Tree; import cn.hutool.core.lang.tree.TreeNodeConfig; import cn.hutool.core.lang.tree.TreeUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.BeanUtils; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; import java.util.Objects; @Service public class OaClassificationServiceImpl extends ServiceImpl3、mapperimplements OaClassificationService { String slash = "/"; String comma = ","; @Resource private OaClassificationMapper classificationMapper; @Override public Long createClassification(ClassificationCreateReqVO reqVO) { // 校验正确性 Long parentId = reqVO.getParentId(); checkCreateOrUpdate(null, parentId, reqVO.getName()); // 插入分类 OaClassification classifiction = new OaClassification(); BeanUtils.copyProperties(reqVO, classifiction); setLevelDept(parentId, classifiction); classificationMapper.insert(classifiction); Long id = classifiction.getId(); if (Objects.equals(classifiction.getParentId(), 0L)) { OaClassification update = new OaClassification(); update.setId(id); update.setDethId(String.valueOf(id)); classificationMapper.updateById(update); } else { OaClassification parentClass = classificationMapper.selectById(classifiction.getParentId()); OaClassification update = new OaClassification(); update.setId(id); update.setDethId(parentClass.getDethId().concat(slash).concat(String.valueOf(id))); classificationMapper.updateById(update); } return id; } @Override public void updateClassification(ClassificationUpdateReqVO reqVO) { Long id = reqVO.getId(); Long parentId = reqVO.getParentId(); // 校验正确性 checkCreateOrUpdate(id, parentId, reqVO.getName()); // 更新分类 OaClassification updateObj = new OaClassification(); BeanUtils.copyProperties(reqVO, updateObj); setLevelDept(parentId, updateObj); classificationMapper.updateById(updateObj); if (!Objects.equals(parentId, 0L)) { OaClassification parentClass = classificationMapper.selectById(parentId); OaClassification update = new OaClassification(); update.setId(id); update.setDethId(parentClass.getDethId().concat(slash).concat(String.valueOf(id))); classificationMapper.updateById(update); } //修改是修改子分类名称 reName(updateObj.getId()); } private void setLevelDept(Long parentId, OaClassification classifiction) { //如果不是一级分类插入层级、纵深 if (parentId == null || Objects.equals(parentId, 0L)) { //设置层级 classifiction.setParentId(0L); classifiction.setDethName(classifiction.getName()); return; } else { OaClassification dbOaClassification = classificationMapper.selectById(parentId); //设置纵深 String deth = dbOaClassification.getDeth(); if (StrUtil.isEmpty(deth)) { classifiction.setDeth(String.valueOf(parentId)); } else { classifiction.setDeth(deth.concat(comma).concat(String.valueOf(parentId))); } classifiction.setDethName(dbOaClassification.getDethName().concat("/").concat(classifiction.getName())); } } @Async public void reName(Long id) { List classificationDOs = classificationMapper.selectByParentId(id); if (classificationDOs == null) { return; } //遍历子类 classificationDOs.forEach((tem) -> { Long parentId = tem.getParentId(); OaClassification dbOaClassification = classificationMapper.selectById(parentId); //设置层级 //设置纵深 String deth = dbOaClassification.getDeth(); if (StrUtil.isEmpty(deth)) { tem.setDeth(String.valueOf(parentId)); } else { tem.setDeth(deth.concat(comma).concat(String.valueOf(parentId))); } tem.setDethName(dbOaClassification.getDethName().concat(slash).concat(tem.getName())); tem.setDethId(dbOaClassification.getDethId().concat(slash).concat(tem.getId().toString())); classificationMapper.updateById(tem); reName(tem.getId()); }); } @Override public void deleteClassification(Long id) { // 校验是否存在 checkClassificationExists(id); // 校验是否有子分类 List oaClassifications = classificationMapper.selectByParentId(id); if (!CollUtil.isEmpty(oaClassifications)) { throw new ServiceException("存在子分类,无法删除"); } classificationMapper.deleteById(id); } @Override public List getSimpleClassifications(ClassificationListReqVO reqVO) { return classificationMapper.selectPageList(reqVO); } @Override public List > getSimpleClassificationsChildren() { // 获得项目分类列表,只要开启状态的 ClassificationListReqVO reqVO = new ClassificationListReqVO(); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); List list = getSimpleClassifications(reqVO); //配置 TreeNodeConfig treeNodeConfig = new TreeNodeConfig(); // 自定义属性名 都要默认值的 // 排序字段,这个字段不能是null,不然会报错,默认最好是数字 treeNodeConfig.setWeightKey("sort"); treeNodeConfig.setIdKey("id"); // 父级id字段 treeNodeConfig.setParentIdKey("parentId"); // 最大递归深度 //treeNodeConfig.setDeep(3); //转换器 List > trees = TreeUtil.build(list, 0, treeNodeConfig, ((object, treeNode) -> { treeNode.setId(object.getId().intValue());//id treeNode.setParentId(object.getParentId().intValue());//父id treeNode.putExtra("name", object.getName()); })); return trees; } @Override public void getClassiByParentId(List list, Long id) { if (id == null) { return; } List classificationDOs = classificationMapper.selectByParentId(id); if (CollUtil.isEmpty(classificationDOs)) { return; } list.addAll(classificationDOs); // 递归,简单粗暴 classificationDOs.forEach((tem) -> { this.getClassiByParentId(list, tem.getId()); }); return; } private void checkCreateOrUpdate(Long id, Long parentId, String name) { // 校验自己存在 checkClassificationExists(id); // 校验父分类的有效性 checkParentClassificationEnable(id, parentId); // 校验分类名的唯一性 checkClassificationNameUnique(id, parentId, name); } private void checkParentClassificationEnable(Long id, Long parentId) { if (parentId == null || Objects.equals(parentId, 0L)) { return; } // 不能设置自己为父分类 if (parentId.equals(id)) { throw new ServiceException("不能设置自己为父分类"); } // 父不存在 OaClassification classifiction = classificationMapper.selectById(parentId); if (classifiction == null) { throw new ServiceException("父级分类不存在"); } // 父分类被禁用 if (!CommonStatusEnum.ENABLE.getStatus().equals(classifiction.getStatus())) { throw new ServiceException("分类不处于开启状态,不允许选择"); } // 父分类不能是原来的子分类 List children = new ArrayList<>(); this.getClassiByParentId(children, id); if (children.stream().anyMatch(classifiction1 -> classifiction1.getId().equals(parentId))) { throw new ServiceException("不能设置自己的子分类为父分类"); } } private void checkClassificationExists(Long id) { if (id == null) { return; } OaClassification classifiction = classificationMapper.selectById(id); if (classifiction == null) { throw new ServiceException("分类不存在"); } } private void checkClassificationNameUnique(Long id, Long parentId, String name) { OaClassification menu = classificationMapper.selectByParentIdAndName(parentId, name); if (menu == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的 if (id == null) { throw new ServiceException("已经存在该名字的分类"); } if (!menu.getId().equals(id)) { throw new ServiceException("已经存在该名字的分类"); } } @Override public OaClassification getClassification(Long id) { return classificationMapper.selectById(id); } }
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import java.util.List; public interface OaClassificationMapper extends BaseMapper{ List selectPageList(@Param("reqVO") ClassificationListReqVO reqVO); OaClassification selectByParentIdAndName(@Param("parentId") Long parentId, @Param("name") String name); List selectByParentId(Long parentId); }
4、基础类id ,name,parent_id, sort,status,deth, deth_name,deth_id,create_by, create_time,update_by,update_time
@Data
@TableName(value = "oa_classification", autoResultMap = true)
@EqualsAndHashCode(callSuper = true)
public class OaClassification extends BaseEntityTwo {
@ApiModelProperty("分类id")
private Long id;
@ApiModelProperty("分类名称")
//@Length(max= 100,message="编码长度不能超过100")
private String name;
@ApiModelProperty("父分类id")
private Long parentId;
@ApiModelProperty("显示顺序")
private Integer sort;
@ApiModelProperty("分类状态(0正常 1停用)")
private Integer status;
@ApiModelProperty("纵深")
//@Length(max= 255,message="编码长度不能超过255")
private String deth;
@ApiModelProperty("纵深名称")
//@Length(max= 300,message="编码长度不能超过300")
private String dethName;
@ApiModelProperty("纵深ID")
//@Length(max= 255,message="编码长度不能超过255")
private String dethId;
}
@Data
public class BaseEntityTwo implements Serializable {
private static final long serialVersionUID = 1L;
@TableField(fill = FieldFill.INSERT)
@Excel(name = "创建者",sort = 101)
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT)
@Excel(name = "创建时间", sort = 102,width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Excel.Type.EXPORT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
@Excel(name = "更新者",sort = 103)
private String updateBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT_UPDATE)
@Excel(name = "更新时间", sort = 104,width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Excel.Type.EXPORT)
private Date updateTime;
}
@Data
public class ClassificationBaseVO {
@ApiModelProperty(value = "分类名称", required = true, example = "xxx")
@NotBlank(message = "分类名称不能为空")
@Size(max = 30, message = "分类名称长度不能超过30个字符")
private String name;
@ApiModelProperty(value = "父菜单ID,一级分类", example = "1024")
private Long parentId;
private Integer sort;
}
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class ClassificationCreateReqVO extends ClassificationBaseVO {
}
@Data
public class ClassificationListReqVO {
@ApiModelProperty(value = "分类名称", example = "王二", notes = "模糊匹配")
private String name;
@ApiModelProperty(value = "分类ID", example = "1")
private Long id;
@ApiModelProperty(value = "展示状态", example = "1", notes = "参见 CommonStatusEnum 枚举类")
private Integer status;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ClassificationSimpleRespVO {
@ApiModelProperty(value = "分类编号", required = true, example = "1024")
private Long id;
@ApiModelProperty(value = "分类名称", required = true, example = "王二")
private String name;
@ApiModelProperty(value = "父分类 ID", required = true, example = "1024")
private Long parentId;
}
@Data
@EqualsAndHashCode(callSuper = true)
public class ClassificationUpdateReqVO extends ClassificationBaseVO {
@ApiModelProperty(value = "分类编号", required = true, example = "1024")
@NotNull(message = "分类编号不能为空")
private Long id;
@ApiModelProperty(value = "状态 0开启 1关闭", required = true, example = "1")
@NotNull(message = "状态不能为空")
private Integer status;
}
四、不足
1、纵深名称和纵深ID使用分隔符+字符存储。可以使用json存储,配合mybatis的typeHandler属性,方便业务使用时结构化;
2、可以增加缓存提升获取分类的效率,如果项目分类比较多,不建议一次取出;



