Skip to content
第 88 / 250 章后端⏱ 10 分钟阅读

第 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 章:操作日志与代码生成

本站基于 VitePress 构建 · 由 Codebook 团队维护