第 88 章:部门与字典模块
学习目标
- 实现部门树(递归查询 + 树形结构)
- 实现字典管理 + 缓存优化
- 完成数据权限配置
一、部门模块
1.1 实体
java
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_dept")
public class Dept extends BaseEntity {
private Long parentId;
private String name;
private String code;
private String leader;
private String phone;
private String email;
private Integer sort;
private Integer status;
}1.2 VO(树形结构)
java
@Data
public class DeptVO {
private Long id;
private Long parentId;
private String name;
private String code;
private String leader;
private String phone;
private String email;
private Integer sort;
private Integer status;
private List<DeptVO> children;
}1.3 Service
java
public interface DeptService extends IService<Dept> {
Long createDept(DeptDTO dto);
void updateDept(Long id, DeptDTO dto);
void deleteDept(Long id);
List<DeptVO> tree();
DeptVO getById(Long id);
}java
@Service
@RequiredArgsConstructor
public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements DeptService {
private final DeptConvert deptConvert;
@Override
@Transactional
public Long createDept(DeptDTO dto) {
Dept dept = deptConvert.toEntity(dto);
dept.setStatus(1);
baseMapper.insert(dept);
return dept.getId();
}
@Override
public void updateDept(Long id, DeptDTO dto) {
Dept dept = baseMapper.selectById(id);
Assert.notNull(dept, "部门不存在");
// ① 防止将父部门设置为自己或子部门
if (dto.getParentId() != null && dto.getParentId() != 0) {
Assert.isTrue(!dto.getParentId().equals(id), "父部门不能是自己");
// 检查是否形成环路
Assert.isTrue(!isDescendant(dto.getParentId(), id), "不能将父部门设为子部门");
}
deptConvert.updateEntity(dto, dept);
baseMapper.updateById(dept);
}
@Override
@Transactional
public void deleteDept(Long id) {
// ① 检查是否有子部门
Long childCount = baseMapper.selectCount(
new LambdaQueryWrapper<Dept>().eq(Dept::getParentId, id));
Assert.isTrue(childCount == 0, "请先删除子部门");
// ② 检查是否有用户
Long userCount = userMapper.countByDeptId(id);
Assert.isTrue(userCount == 0, "部门下存在用户,无法删除");
baseMapper.deleteById(id);
}
@Override
public List<DeptVO> tree() {
List<Dept> all = baseMapper.selectList(
new LambdaQueryWrapper<Dept>().orderByAsc(Dept::getSort));
return buildTree(all, 0L);
}
@Override
public DeptVO getById(Long id) {
Dept dept = baseMapper.selectById(id);
Assert.notNull(dept, "部门不存在");
return deptConvert.toVO(dept);
}
/**
* 判断 ancestorId 是否为 deptId 的后代
*/
private boolean isDescendant(Long ancestorId, Long deptId) {
Dept parent = baseMapper.selectById(ancestorId);
while (parent != null && parent.getParentId() != 0) {
if (parent.getId().equals(deptId)) return true;
parent = baseMapper.selectById(parent.getParentId());
}
return false;
}
private List<DeptVO> buildTree(List<Dept> all, Long parentId) {
return all.stream()
.filter(d -> d.getParentId().equals(parentId))
.map(d -> {
DeptVO vo = deptConvert.toVO(d);
vo.setChildren(buildTree(all, d.getId()));
return vo;
})
.toList();
}
}1.4 Controller
java
@RestController
@RequestMapping("/api/dept")
@RequiredArgsConstructor
@Tag(name = "部门管理")
public class DeptController {
private final DeptService deptService;
@GetMapping("/tree")
@Operation(summary = "部门树")
public Result<List<DeptVO>> tree() {
return Result.ok(deptService.tree());
}
@GetMapping("/{id}")
public Result<DeptVO> getById(@PathVariable Long id) {
return Result.ok(deptService.getById(id));
}
@PostMapping
@RequiresPermission("dept:create")
public Result<Long> create(@RequestBody @Valid DeptDTO dto) {
return Result.ok(deptService.createDept(dto));
}
@PutMapping("/{id}")
@RequiresPermission("dept:update")
public Result<Void> update(@PathVariable Long id, @RequestBody DeptDTO dto) {
deptService.updateDept(id, dto);
return Result.ok();
}
@DeleteMapping("/{id}")
@RequiresPermission("dept:delete")
public Result<Void> delete(@PathVariable Long id) {
deptService.deleteDept(id);
return Result.ok();
}
}二、字典模块
2.1 实体
java
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_dict")
public class Dict extends BaseEntity {
private String typeCode;
private String label;
private String value;
private Integer sort;
private Integer status;
private String remark;
}2.2 VO
java
@Data
public class DictVO {
private Long id;
private String typeCode;
private String label;
private String value;
private Integer sort;
}
@Data
public class DictTypeVO {
private String typeCode;
private String description;
private List<DictVO> items;
}2.3 Service(缓存优化)
java
public interface DictService extends IService<Dict> {
List<DictVO> getByType(String typeCode);
Map<String, List<DictVO>> getAll();
void refreshCache();
}java
@Service
@RequiredArgsConstructor
public class DictServiceImpl extends ServiceImpl<DictMapper, Dict> implements DictService {
private final RedisTemplate<String, Object> redis;
private static final String DICT_KEY = "dict:all";
private static final Duration CACHE_TTL = Duration.ofHours(2);
@Override
public List<DictVO> getByType(String typeCode) {
Map<String, List<DictVO>> all = getAll();
return all.getOrDefault(typeCode, List.of());
}
@Override
public Map<String, List<DictVO>> getAll() {
// ① 查缓存
Object cached = redis.opsForValue().get(DICT_KEY);
if (cached instanceof Map<?, ?> map) {
return (Map<String, List<DictVO>>) map;
}
// ② 查 DB
List<Dict> all = baseMapper.selectList(
new LambdaQueryWrapper<Dict>()
.eq(Dict::getStatus, 1)
.orderByAsc(Dict::getSort));
Map<String, List<DictVO>> grouped = all.stream()
.collect(Collectors.groupingBy(
Dict::getTypeCode,
Collectors.mapping(this::toVO, Collectors.toList())));
// ③ 写缓存
redis.opsForValue().set(DICT_KEY, grouped, CACHE_TTL);
return grouped;
}
@Override
public void refreshCache() {
redis.delete(DICT_KEY);
}
@PostMapping
@RequiresPermission("dict:create")
public Result<Long> create(@RequestBody DictDTO dto) {
Long id = dictService.createDict(dto);
dictService.refreshCache();
return Result.ok(id);
}
private DictVO toVO(Dict dict) {
DictVO vo = new DictVO();
BeanUtils.copyProperties(dict, vo);
return vo;
}
}2.4 Controller
java
@RestController
@RequestMapping("/api/dict")
@RequiredArgsConstructor
@Tag(name = "字典管理")
public class DictController {
private final DictService dictService;
@GetMapping("/{typeCode}")
@Operation(summary = "根据类型查询字典")
public Result<List<DictVO>> getByType(@PathVariable String typeCode) {
return Result.ok(dictService.getByType(typeCode));
}
@GetMapping("/all")
@Operation(summary = "查询所有字典")
public Result<Map<String, List<DictVO>>> all() {
return Result.ok(dictService.getAll());
}
@PostMapping
@RequiresPermission("dict:create")
public Result<Long> create(@RequestBody @Valid DictDTO dto) {
return Result.ok(dictService.createDict(dto));
}
@PutMapping("/{id}")
@RequiresPermission("dict:update")
public Result<Void> update(@PathVariable Long id, @RequestBody DictDTO dto) {
dictService.updateDict(id, dto);
dictService.refreshCache();
return Result.ok();
}
@DeleteMapping("/{id}")
@RequiresPermission("dict:delete")
public Result<Void> delete(@PathVariable Long id) {
dictService.deleteDict(id);
dictService.refreshCache();
return Result.ok();
}
}三、数据权限进阶
3.1 数据权限注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
/** 部门字段名,默认 dept_id */
String deptField() default "dept_id";
/** 用户字段名,默认 create_by */
String userField() default "create_by";
}3.2 MyBatis-Plus 拦截器
java
@Component
@RequiredArgsConstructor
public class DataScopeInnerInterceptor implements InnerInterceptor {
private final SecurityUtils securityUtils;
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// ① 检查方法上是否有 @DataScope 注解
DataScope dataScope = getDataScopeAnnotation(ms);
if (dataScope == null) return;
// ② 获取当前用户数据权限
User user = securityUtils.getCurrentUser();
if (user.getRoles() == null || user.getRoles().isEmpty()) return;
Role role = user.getRoles().get(0); // 取主角色
if (role.getDataScope() == null) return;
// ③ 根据数据权限范围拼接 SQL
String sql = boundSql.getSql();
String dataScopeCondition = buildCondition(role, user, dataScope);
if (dataScopeCondition != null) {
String newSql = injectCondition(sql, dataScopeCondition);
// 用反射改写 SQL(简化版)
ReflectUtil.setFieldValue(boundSql, "sql", newSql);
}
}
private String buildCondition(Role role, User user, DataScope annotation) {
String deptField = annotation.deptField();
String userField = annotation.userField();
switch (role.getDataScope()) {
case 1: // 全部
return null;
case 2: // 本部门及下级
List<Long> deptIds = getDeptAndChildren(user.getDeptId());
return deptField + " IN (" + String.join(",",
deptIds.stream().map(String::valueOf).toList()) + ")";
case 3: // 本部门
return deptField + " = " + user.getDeptId();
case 4: // 仅本人
return userField + " = " + user.getId();
case 5: // 自定义
List<Long> customDeptIds = roleDeptMapper.selectDeptIdsByRoleId(role.getId());
return deptField + " IN (" + String.join(",",
customDeptIds.stream().map(String::valueOf).toList()) + ")";
default:
return null;
}
}
private List<Long> getDeptAndChildren(Long deptId) {
// 递归查询所有子部门
return deptMapper.selectDescendantIds(deptId);
}
private DataScope getDataScopeAnnotation(MappedStatement ms) {
// 通过反射获取 Mapper 方法上的注解(略)
return null;
}
private String injectCondition(String sql, String condition) {
if (sql.contains("WHERE")) {
return sql.replaceFirst("WHERE", "WHERE " + condition + " AND");
}
return sql + " WHERE " + condition;
}
}四、字典在前端的使用
javascript
// src/store/dict.js
import { defineStore } from 'pinia'
import api from '@/api'
export const useDictStore = defineStore('dict', {
state: () => ({
dictMap: {}, // { user_status: [{value, label}], ... }
}),
actions: {
async loadDicts() {
const { data } = await api.getDictAll()
this.dictMap = data
},
getLabel(typeCode, value) {
const items = this.dictMap[typeCode] || []
const item = items.find(i => i.value === String(value))
return item ? item.label : value
},
},
})vue
<template>
<el-tag :type="statusType">{{ dictStore.getLabel('user_status', row.status) }}</el-tag>
</template>
<script setup>
import { useDictStore } from '@/store/dict'
const dictStore = useDictStore()
</script>五、本章小结
| 要点 | 关键 |
|---|---|
| 部门 | 树形结构、递归查询、防环路 |
| 字典 | 分类管理 + Redis 缓存 |
| 数据权限 | 注解 + MyBatis 拦截器 |
| 防环路 | 不能将父部门设为子部门 |
| 前端集成 | Pinia store + 全局方法 |
动手练习
练习 1:基础题
实现部门树接口:递归查询所有部门,返回树形结构。
练习 2:进阶题
实现字典管理:CRUD + 缓存,用户查询字典时直接命中 Redis。
练习 3:思考题
数据权限有 5 种范围(全/本部门及下级/本部门/仅本人/自定义)。如何用装饰器模式或策略模式优雅实现?
下一章:第 89 章:操作日志与代码生成 →