parentIds) {
+ return selectList(CategoryDO::getPid, parentIds);
+ }
+}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java
index 8b9e7681..fe953907 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java
@@ -17,6 +17,14 @@ public interface RedisKeyConstants {
*/
String DEPT_CHILDREN_ID_LIST = "dept_children_ids";
+ /**
+ * 指定分类字典的所有子字典编号数组的缓存
+ *
+ * KEY 格式:category_children_ids:{id}
+ * VALUE 数据类型:String 子字典编号集合
+ */
+ String CATEGORY_CHILDREN_ID_LIST = "category_children_ids";
+
/**
* 角色的缓存
*
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/CategoryService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/CategoryService.java
new file mode 100644
index 00000000..38a3f047
--- /dev/null
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/CategoryService.java
@@ -0,0 +1,110 @@
+package cn.iocoder.yudao.module.system.service.dict;
+
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.dict.CategoryDO;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 分类字典 Service 接口
+ *
+ * @author vinjor-m
+ */
+public interface CategoryService {
+
+ /**
+ * 创建分类字典
+ *
+ * @param categoryDO 分类字典信息
+ * @return 分类字典主键id
+ */
+ String createCategory(CategoryDO categoryDO);
+
+ /**
+ * 更新分类字典
+ *
+ * @param categoryDO 分类字典信息
+ */
+ void updateCategory(CategoryDO categoryDO);
+
+ /**
+ * 删除分类字典
+ *
+ * @param id 分类字典id
+ */
+ void deleteCategory(String id);
+
+ /**
+ * 批量删除分类字典
+ * @author vinjor-M
+ * @date 17:16 2024/8/3
+ * @param ids 分类字典ids
+ **/
+ void deleteCategory(Set ids);
+
+ /**
+ * 获得分类字典信息
+ *
+ * @param id 分类字典编号
+ * @return 分类字典信息
+ */
+ CategoryDO getCategory(String id);
+
+ /**
+ * 获得分类字典信息数组
+ *
+ * @param ids 分类字典id数组
+ * @return 分类字典信息数组
+ */
+ List getCategoryList(Collection ids);
+
+ /**
+ * 筛选分类字典列表
+ *
+ * @param reqVO 筛选条件请求 VO
+ * @return 分类字典列表
+ */
+ List getCategoryList(CategoryDO reqVO);
+
+ /**
+ * 获得指定编号的分类字典 Map
+ *
+ * @param ids 分类字典id数组
+ * @return 分类字典 Map
+ */
+ default Map getCategoryMap(Collection ids) {
+ List list = getCategoryList(ids);
+ return CollectionUtils.convertMap(list, CategoryDO::getId);
+ }
+
+ /**
+ * 获得指定分类字典的所有子分类字典
+ *
+ * @param id 分类字典id
+ * @return 子分类字典列表
+ */
+ List getChildCategoryList(String id);
+
+ /**
+ * 获得所有子分类字典,从缓存中
+ *
+ * @param id 父分类字典id
+ * @return 子分类字典列表
+ */
+ Set getChildCategoryIdListFromCache(String id);
+
+ /**
+ * 校验分类字典们是否有效。如下情况,视为无效:
+ * 1. 分类字典id不存在
+ *
+ * @param ids 分类字典id数组
+ */
+ void validateCategoryList(Collection ids);
+
+}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/CategoryServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/CategoryServiceImpl.java
new file mode 100644
index 00000000..e2e59614
--- /dev/null
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/CategoryServiceImpl.java
@@ -0,0 +1,275 @@
+package cn.iocoder.yudao.module.system.service.dict;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.dict.CategoryDO;
+import cn.iocoder.yudao.module.system.dal.mysql.dept.DeptMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.dict.CategoryMapper;
+import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
+import com.google.common.annotations.VisibleForTesting;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.DEPT_NAME_DUPLICATE;
+
+/**
+ * 分类字典 Service 实现类
+ *
+ * @author vinjor-m
+ */
+@Service
+@Validated
+@Slf4j
+public class CategoryServiceImpl implements CategoryService {
+ @Resource
+ private CategoryMapper categoryMapper;
+ /**
+ * 创建分类字典
+ *
+ * @param categoryDO 分类字典信息
+ * @return 分类字典主键id
+ */
+ @Override
+ @CacheEvict(cacheNames = RedisKeyConstants.CATEGORY_CHILDREN_ID_LIST,
+ allEntries = true) // allEntries 清空所有缓存,因为操作一个分类字典,涉及到多个缓存
+ public String createCategory(CategoryDO categoryDO) {
+ if (categoryDO.getPid() == null) {
+ categoryDO.setPid(CategoryDO.PARENT_ID_ROOT);
+ }
+ // 校验父级的有效性
+ validateParentCategory(null, categoryDO.getPid());
+ // 校验父级的唯一性
+ validateCategoryCodeUnique(null, categoryDO.getPid(), categoryDO.getCode());
+
+ // 插入字典
+ categoryMapper.insert(categoryDO);
+ return categoryDO.getId();
+ }
+
+ /**
+ * 更新分类字典
+ *
+ * @param updateReqVO 分类字典信息
+ */
+ @Override
+ @CacheEvict(cacheNames = RedisKeyConstants.CATEGORY_CHILDREN_ID_LIST,
+ allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存
+ public void updateCategory(CategoryDO updateReqVO) {
+ if (updateReqVO.getPid() == null) {
+ updateReqVO.setPid(CategoryDO.PARENT_ID_ROOT);
+ }
+ // 校验自己存在
+ validateCategoryExists(updateReqVO.getId());
+ // 校验父级的有效性
+ validateParentCategory(updateReqVO.getId(), updateReqVO.getPid());
+ // 校验code的唯一性
+ validateCategoryCodeUnique(updateReqVO.getId(), updateReqVO.getPid(), updateReqVO.getCode());
+
+ // 更新
+ categoryMapper.updateById(updateReqVO);
+ }
+
+ /**
+ * 删除分类字典
+ *
+ * @param id 分类字典id
+ */
+ @Override
+ @CacheEvict(cacheNames = RedisKeyConstants.CATEGORY_CHILDREN_ID_LIST,
+ allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存
+ public void deleteCategory(String id) {
+ // 校验是否存在
+ validateCategoryExists(id);
+ // 校验是否有子部门
+ if (categoryMapper.selectCountByParentId(id) > 0) {
+ throw exception(CATEGORY_EXITS_CHILDREN);
+ }
+ // 删除部门
+ categoryMapper.deleteById(id);
+ }
+
+ /**
+ * 批量删除分类字典
+ *
+ * @param ids 分类字典ids
+ * @author vinjor-M
+ * @date 17:16 2024/8/3
+ **/
+ @Override
+ @CacheEvict(cacheNames = RedisKeyConstants.CATEGORY_CHILDREN_ID_LIST,
+ allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存
+ public void deleteCategory(Set ids) {
+ categoryMapper.deleteByIds(ids);
+ }
+
+ @VisibleForTesting
+ void validateCategoryExists(String id) {
+ if (id == null) {
+ return;
+ }
+ CategoryDO categoryDO = categoryMapper.selectById(id);
+ if (categoryDO == null) {
+ throw exception(CATEGORY_NOT_FOUND);
+ }
+ }
+
+ @VisibleForTesting
+ void validateParentCategory(String id, String parentId) {
+ if (parentId == null || CategoryDO.PARENT_ID_ROOT.equals(parentId)) {
+ return;
+ }
+ // 1. 不能设置自己为父级
+ if (Objects.equals(id, parentId)) {
+ throw exception(CATEGORY_PARENT_ERROR);
+ }
+ // 2. 父级不存在
+ CategoryDO parent = categoryMapper.selectById(parentId);
+ if (parent == null) {
+ throw exception(CATEGORY_PARENT_NOT_EXITS);
+ }
+ // 3. 递归校验父级,如果父级是自己的子级,则报错,避免形成环路
+ if (id == null) { // id 为空,说明新增,不需要考虑环路
+ return;
+ }
+ for (int i = 0; i < Short.MAX_VALUE; i++) {
+ // 3.1 校验环路
+ parentId = parent.getPid();
+ if (Objects.equals(id, parentId)) {
+ throw exception(CATEGORY_PARENT_IS_CHILD);
+ }
+ // 3.2 继续递归下一级父级
+ if (parentId == null || CategoryDO.PARENT_ID_ROOT.equals(parentId)) {
+ break;
+ }
+ parent = categoryMapper.selectById(parentId);
+ if (parent == null) {
+ break;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void validateCategoryCodeUnique(String id, String parentId, String code) {
+ CategoryDO categoryDO = categoryMapper.selectByParentIdAndCode(parentId, code);
+ if (categoryDO == null) {
+ return;
+ }
+ // 如果 id 为空,说明不用比较是否为相同 id 的code
+ if (id == null) {
+ throw exception(CATEGORY_CODE_DUPLICATE);
+ }
+ if (ObjectUtil.notEqual(categoryDO.getId(), id)) {
+ throw exception(CATEGORY_CODE_DUPLICATE);
+ }
+ }
+
+ /**
+ * 获得分类字典信息
+ *
+ * @param id 分类字典编号
+ * @return 分类字典信息
+ */
+ @Override
+ public CategoryDO getCategory(String id) {
+ return categoryMapper.selectById(id);
+ }
+
+ /**
+ * 获得分类字典信息数组
+ *
+ * @param ids 分类字典id数组
+ * @return 分类字典信息数组
+ */
+ @Override
+ public List getCategoryList(Collection ids) {
+ if (CollUtil.isEmpty(ids)) {
+ return Collections.emptyList();
+ }
+ return categoryMapper.selectBatchIds(ids);
+ }
+
+ /**
+ * 筛选分类字典列表
+ *
+ * @param reqVO 筛选条件请求 VO
+ * @return 分类字典列表
+ */
+ @Override
+ public List getCategoryList(CategoryDO reqVO) {
+ return categoryMapper.selectList(reqVO);
+ }
+
+ /**
+ * 获得指定分类字典的所有子分类字典
+ *
+ * @param id 分类字典id
+ * @return 子分类字典列表
+ */
+ @Override
+ public List getChildCategoryList(String id) {
+ List children = new LinkedList<>();
+ // 遍历每一层
+ Collection parentIds = Collections.singleton(id);
+ for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下,存在死循环
+ // 查询当前层,所有的子部门
+ List catgs = categoryMapper.selectListByParentId(parentIds);
+ // 1. 如果没有子部门,则结束遍历
+ if (CollUtil.isEmpty(catgs)) {
+ break;
+ }
+ // 2. 如果有子部门,继续遍历
+ children.addAll(catgs);
+ parentIds = convertSet(catgs, CategoryDO::getId);
+ }
+ return children;
+ }
+
+ /**
+ * 获得所有子分类字典,从缓存中
+ *
+ * @param id 父分类字典id
+ * @return 子分类字典列表
+ */
+ @Override
+ @DataPermission(enable = false) // 禁用数据权限,避免建立不正确的缓存
+ @Cacheable(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, key = "#id")
+ public Set getChildCategoryIdListFromCache(String id) {
+ List children = getChildCategoryList(id);
+ return convertSet(children, CategoryDO::getId);
+ }
+
+ /**
+ * 校验分类字典们是否有效。如下情况,视为无效:
+ * 1. 分类字典id不存在
+ *
+ * @param ids 分类字典id数组
+ */
+ @Override
+ public void validateCategoryList(Collection ids) {
+ if (CollUtil.isEmpty(ids)) {
+ return;
+ }
+ // 获得字典信息
+ Map catgMap = getCategoryMap(ids);
+ // 校验
+ ids.forEach(id -> {
+ CategoryDO catg = catgMap.get(id);
+ if (catg == null) {
+ throw exception(CATEGORY_NOT_FOUND);
+ }
+ });
+ }
+}
diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml
index efcf0b37..8ea7f758 100644
--- a/yudao-server/src/main/resources/application.yaml
+++ b/yudao-server/src/main/resources/application.yaml
@@ -254,6 +254,7 @@ yudao:
- system_tenant
- system_tenant_package
- system_service_package
+ - system_category
- system_dict_data
- system_dict_type
- system_error_code