如何实现多级分类,方便维护,容易获取子分类、父级分类、某个子分类的顶级分类的所有子分类;
一、数据建表
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主要依赖
<!-- Mysql驱动包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--ORM--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.0</version> </dependency>
三、代码
1、controller
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<OaClassification> list = classificationService.getSimpleClassifications(reqVO); //配置 TreeNodeConfig treeNodeConfig = new TreeNodeConfig(); // 自定义属性名 都要默认值的 // 排序字段,这个字段不能是null,不然会报错,默认最好是数字 treeNodeConfig.setWeightKey("sort"); treeNodeConfig.setIdKey("id"); // 父级id字段 treeNodeConfig.setParentIdKey("parentId"); // 最大递归深度 //treeNodeConfig.setDeep(3); //转换器 List<Tree<Integer>> 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<Tree<Integer>> 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<OaClassification> { /** * 创建分类 * * @param reqVO 分类信息 * @return 分类编号 */ Long createClassification(ClassificationCreateReqVO reqVO); /** * 更新分类 * * @param reqVO 分类信息 */ void updateClassification(ClassificationUpdateReqVO reqVO); /** * 删除分类 * * @param id 分类编号 */ void deleteClassification(Long id); /** * 筛选分类列表 * * @param reqVO 筛选条件请求 VO * @return 分类列表 */ List<OaClassification> getSimpleClassifications(ClassificationListReqVO reqVO); /** *获取项目分类精简信息列表-子分类 * @return */ List<Tree<Integer>> getSimpleClassificationsChildren(); /** * 获得所有子分类,从缓存中 * * @param parentId 分类编号 * @param list 是否递归获取所有 * @return 子分类列表 */ void getClassiByParentId( List<OaClassification> list,Long parentId); /** * 获得分类信息 * * @param id 分类编号 * @return 分类信息 */ 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 ServiceImpl<OaClassificationMapper, OaClassification> implements 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()); } /** * @param parentId 父级ID * @param classifiction */ 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<OaClassification> 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<OaClassification> oaClassifications = classificationMapper.selectByParentId(id); if (!CollUtil.isEmpty(oaClassifications)) { throw new ServiceException("存在子分类,无法删除"); } classificationMapper.deleteById(id); } @Override public List<OaClassification> getSimpleClassifications(ClassificationListReqVO reqVO) { return classificationMapper.selectPageList(reqVO); } @Override public List<Tree<Integer>> getSimpleClassificationsChildren() { // 获得项目分类列表,只要开启状态的 ClassificationListReqVO reqVO = new ClassificationListReqVO(); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); List<OaClassification> list = getSimpleClassifications(reqVO); //配置 TreeNodeConfig treeNodeConfig = new TreeNodeConfig(); // 自定义属性名 都要默认值的 // 排序字段,这个字段不能是null,不然会报错,默认最好是数字 treeNodeConfig.setWeightKey("sort"); treeNodeConfig.setIdKey("id"); // 父级id字段 treeNodeConfig.setParentIdKey("parentId"); // 最大递归深度 //treeNodeConfig.setDeep(3); //转换器 List<Tree<Integer>> 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<OaClassification> list, Long id) { if (id == null) { return; } List<OaClassification> 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<OaClassification> 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); } }
3、mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import java.util.List; /** * @Entity generator.domain.OaClassification */ public interface OaClassificationMapper extends BaseMapper<OaClassification> { List<OaClassification> selectPageList(@Param("reqVO") ClassificationListReqVO reqVO); OaClassification selectByParentIdAndName(@Param("parentId") Long parentId, @Param("name") String name); List<OaClassification> selectByParentId(Long parentId); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.xxxx.mapper.OaClassificationMapper"> <resultMap id="BaseResultMap" type="com.xxxx.domain.OaClassification"> <id property="id" column="id" jdbcType="BIGINT"/> <result property="name" column="name" jdbcType="VARCHAR"/> <result property="parentId" column="parent_id" jdbcType="BIGINT"/> <result property="sort" column="sort" jdbcType="INTEGER"/> <result property="status" column="status" jdbcType="TINYINT"/> <result property="deth" column="deth" jdbcType="VARCHAR"/> <result property="dethName" column="deth_name" jdbcType="VARCHAR"/> <result property="dethId" column="deth_id" jdbcType="VARCHAR"/> <result property="createBy" column="create_by" jdbcType="VARCHAR"/> <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/> <result property="updateBy" column="update_by" jdbcType="VARCHAR"/> <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/> </resultMap> <sql id="Base_Column_List"> id ,name,parent_id, sort,status,deth, deth_name,deth_id,create_by, create_time,update_by,update_time </sql> <select id="selectPageList" resultType="com.xxxx.domain.OaClassification"> select * from oa_classification <where> <if test="reqVO.id != null "> id= #{reqVO.name} </if> <if test="reqVO.name != null and reqVO.name != ''"> and name like (concat('%', #{reqVO.name}, '%')) </if> <if test="reqVO.status != null "> and status= #{reqVO.status} </if> </where> order by sort desc,id desc </select> <select id="selectByParentIdAndName" resultType="com.xxxx.domain.OaClassification"> select * from oa_classification <where> <if test="parentId != null "> parent_id= #{parentId} </if> <if test="name != null and name != ''"> and name =#{name} </if> </where> </select> <select id="selectByParentId" resultType="com.xxxx.domain.OaClassification"> select * from oa_classification <where> <if test="parentId != null "> parent_id= #{parentId} </if> </where> </select> </mapper>
4、基础类
1.@Data @TableName(value = "oa_classification", autoResultMap = true) @EqualsAndHashCode(callSuper = true) public class OaClassification extends BaseEntityTwo { /** * 分类id */ @ApiModelProperty("分类id") private Long id; /** * 分类名称 */ @ApiModelProperty("分类名称") //@Length(max= 100,message="编码长度不能超过100") private String name; /** * 父分类id */ @ApiModelProperty("父分类id") private Long parentId; /** * 显示顺序 */ @ApiModelProperty("显示顺序") private Integer sort; /** * 分类状态(0正常 1停用) */ @ApiModelProperty("分类状态(0正常 1停用)") private Integer status; /** * 纵深 */ @ApiModelProperty("纵深") //@Length(max= 255,message="编码长度不能超过255") private String deth; /** * 纵深名称 */ @ApiModelProperty("纵深名称") //@Length(max= 300,message="编码长度不能超过300") private String dethName; /** * 纵深ID */ @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、可以增加缓存提升获取分类的效率,如果项目分类比较多,不建议一次取出;