This commit is contained in:
xiao-fajia 2024-08-07 16:08:12 +08:00
commit 5be2781ce1
8 changed files with 546 additions and 0 deletions

View File

@ -125,6 +125,14 @@ public interface ErrorCodeConstants {
ErrorCode SOCIAL_CLIENT_NOT_EXISTS = new ErrorCode(1_002_018_202, "社交客户端不存在");
ErrorCode SOCIAL_CLIENT_UNIQUE = new ErrorCode(1_002_018_203, "社交客户端已存在配置");
// ========== 分类字典模块 1-002-019-000 ==========
ErrorCode CATEGORY_CODE_DUPLICATE = new ErrorCode(1_002_019_000, "已经存在相同编码的字典");
ErrorCode CATEGORY_PARENT_NOT_EXITS = new ErrorCode(1_002_019_001,"父级字典不存在");
ErrorCode CATEGORY_NOT_FOUND = new ErrorCode(1_002_019_002, "当前字典不存在");
ErrorCode CATEGORY_EXITS_CHILDREN = new ErrorCode(1_002_019_003, "存在子级字典,无法删除");
ErrorCode CATEGORY_PARENT_ERROR = new ErrorCode(1_002_019_004, "不能设置自己为父级字典");
ErrorCode CATEGORY_PARENT_IS_CHILD = new ErrorCode(1_002_019_007, "不能设置自己的子级为父级");
// ========== OAuth2 客户端 1-002-020-000 =========
ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1_002_020_000, "OAuth2 客户端不存在");

View File

@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.system.controller.admin.dict;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.CategoryDO;
import cn.iocoder.yudao.module.system.service.dict.CategoryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 分类字典")
@RestController
@RequestMapping("/system/category")
@Validated
public class CategoryController {
@Resource
private CategoryService categoryService;
@PostMapping("create")
@Operation(summary = "创建分类字典")
@PreAuthorize("@ss.hasPermission('system:category:create')")
public CommonResult<String> createDept(@Valid @RequestBody CategoryDO createReqVO) {
String id = categoryService.createCategory(createReqVO);
return success(id);
}
@PutMapping("update")
@Operation(summary = "更新分类字典")
@PreAuthorize("@ss.hasPermission('system:category:update')")
public CommonResult<Boolean> updateDept(@Valid @RequestBody CategoryDO updateReqVO) {
categoryService.updateCategory(updateReqVO);
return success(true);
}
@DeleteMapping("delete")
@Operation(summary = "删除分类字典")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:category:delete')")
public CommonResult<Boolean> deleteDept(@RequestParam("id") String id) {
categoryService.deleteCategory(id);
return success(true);
}
@GetMapping("/list")
@Operation(summary = "获取分类字典列表")
@PreAuthorize("@ss.hasPermission('system:category:query')")
public CommonResult<List<CategoryDO>> getDeptList(CategoryDO reqVO) {
return success(categoryService.getCategoryList(reqVO));
}
@GetMapping("/get")
@Operation(summary = "获得分类字典信息")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:category:query')")
public CommonResult<CategoryDO> getDept(@RequestParam("id") String id) {
return success(categoryService.getCategory(id));
}
}

View File

@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.system.dal.dataobject.dict;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
/**
* 分类字典
*
* @author vinjor-m
*/
@TableName("system_category")
@Data
@EqualsAndHashCode(callSuper = true)
public class CategoryDO extends BaseDO {
public static final String PARENT_ID_ROOT = "0";
/**主键*/
@TableId(type = IdType.ASSIGN_ID)
private java.lang.String id;
/**父级节点*/
private java.lang.String pid;
/**类型名称*/
private java.lang.String name;
/**类型编码*/
private java.lang.String code;
/**排序*/
private Integer sort;
/**备注*/
private java.lang.String remark;
}

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.system.dal.mysql.dict;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.CategoryDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
public interface CategoryMapper extends BaseMapperX<CategoryDO> {
default List<CategoryDO> selectList(CategoryDO reqVO) {
return selectList(new LambdaQueryWrapperX<CategoryDO>()
.likeIfPresent(CategoryDO::getName, reqVO.getName())
.likeIfPresent(CategoryDO::getCode, reqVO.getCode())
.orderByAsc(CategoryDO::getSort));
}
default CategoryDO selectByParentIdAndCode(String parentId, String code) {
return selectOne(CategoryDO::getPid, parentId, CategoryDO::getCode,code);
}
default Long selectCountByParentId(String parentId) {
return selectCount(CategoryDO::getPid, parentId);
}
default List<CategoryDO> selectListByParentId(Collection<String> parentIds) {
return selectList(CategoryDO::getPid, parentIds);
}
}

View File

@ -17,6 +17,14 @@ public interface RedisKeyConstants {
*/
String DEPT_CHILDREN_ID_LIST = "dept_children_ids";
/**
* 指定分类字典的所有子字典编号数组的缓存
* <p>
* KEY 格式category_children_ids:{id}
* VALUE 数据类型String 子字典编号集合
*/
String CATEGORY_CHILDREN_ID_LIST = "category_children_ids";
/**
* 角色的缓存
* <p>

View File

@ -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<String> ids);
/**
* 获得分类字典信息
*
* @param id 分类字典编号
* @return 分类字典信息
*/
CategoryDO getCategory(String id);
/**
* 获得分类字典信息数组
*
* @param ids 分类字典id数组
* @return 分类字典信息数组
*/
List<CategoryDO> getCategoryList(Collection<String> ids);
/**
* 筛选分类字典列表
*
* @param reqVO 筛选条件请求 VO
* @return 分类字典列表
*/
List<CategoryDO> getCategoryList(CategoryDO reqVO);
/**
* 获得指定编号的分类字典 Map
*
* @param ids 分类字典id数组
* @return 分类字典 Map
*/
default Map<String, CategoryDO> getCategoryMap(Collection<String> ids) {
List<CategoryDO> list = getCategoryList(ids);
return CollectionUtils.convertMap(list, CategoryDO::getId);
}
/**
* 获得指定分类字典的所有子分类字典
*
* @param id 分类字典id
* @return 子分类字典列表
*/
List<CategoryDO> getChildCategoryList(String id);
/**
* 获得所有子分类字典从缓存中
*
* @param id 父分类字典id
* @return 子分类字典列表
*/
Set<String> getChildCategoryIdListFromCache(String id);
/**
* 校验分类字典们是否有效如下情况视为无效
* 1. 分类字典id不存在
*
* @param ids 分类字典id数组
*/
void validateCategoryList(Collection<String> ids);
}

View File

@ -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<String> 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<CategoryDO> getCategoryList(Collection<String> ids) {
if (CollUtil.isEmpty(ids)) {
return Collections.emptyList();
}
return categoryMapper.selectBatchIds(ids);
}
/**
* 筛选分类字典列表
*
* @param reqVO 筛选条件请求 VO
* @return 分类字典列表
*/
@Override
public List<CategoryDO> getCategoryList(CategoryDO reqVO) {
return categoryMapper.selectList(reqVO);
}
/**
* 获得指定分类字典的所有子分类字典
*
* @param id 分类字典id
* @return 子分类字典列表
*/
@Override
public List<CategoryDO> getChildCategoryList(String id) {
List<CategoryDO> children = new LinkedList<>();
// 遍历每一层
Collection<String> parentIds = Collections.singleton(id);
for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下存在死循环
// 查询当前层所有的子部门
List<CategoryDO> 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<String> getChildCategoryIdListFromCache(String id) {
List<CategoryDO> children = getChildCategoryList(id);
return convertSet(children, CategoryDO::getId);
}
/**
* 校验分类字典们是否有效如下情况视为无效
* 1. 分类字典id不存在
*
* @param ids 分类字典id数组
*/
@Override
public void validateCategoryList(Collection<String> ids) {
if (CollUtil.isEmpty(ids)) {
return;
}
// 获得字典信息
Map<String, CategoryDO> catgMap = getCategoryMap(ids);
// 校验
ids.forEach(id -> {
CategoryDO catg = catgMap.get(id);
if (catg == null) {
throw exception(CATEGORY_NOT_FOUND);
}
});
}
}

View File

@ -254,6 +254,7 @@ yudao:
- system_tenant
- system_tenant_package
- system_service_package
- system_category
- system_dict_data
- system_dict_type
- system_error_code