第 8 章:角色模块
学习目标
- 实现角色的 CRUD 接口
- 实现"角色绑定菜单"功能
- 实现"角色绑定用户"功能
一、角色实体
java
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_role")
public class Role extends BaseEntity {
private String name; // 角色名:超级管理员
private String code; // 编码:ROLE_ADMIN
private String description;
private Integer sort;
private Integer status; // 0=禁用 1=正常
private Integer dataScope; // 1-5 数据权限范围
}⚠️ 坑 1:角色编码
code用大写常量约定 (ROLE_ADMIN)。前端可以通过v-if="hasRole('ADMIN')"隐藏菜单。命名约定保持前后端一致,别一会ADMIN一会SuperAdmin。
二、DTO
java
@Data
public class RoleCreateDTO {
@NotBlank(message = "角色名不能为空")
@Length(max = 50)
private String name;
@NotBlank(message = "角色编码不能为空")
@Pattern(regexp = "^ROLE_[A-Z_]+$",
message = "编码格式:ROLE_后跟大写字母和下划线")
private String code;
private String description;
@Min(0)
private Integer sort = 0;
private Integer dataScope = 4; // 默认仅本人
private List<Long> menuIds; // 创建时直接分配菜单
}dataScope 五个值:
| 值 | 含义 |
|---|---|
| 1 | 全部数据 |
| 2 | 本部门及下级 |
| 3 | 本部门 |
| 4 | 仅本人 |
| 5 | 自定义(配合 sys_role_dept) |
三、Service
java
public interface RoleService extends IService<Role> {
Long createRole(RoleCreateDTO dto);
void updateRole(Long id, RoleUpdateDTO dto);
void deleteRole(Long id);
void assignMenus(Long roleId, List<Long> menuIds);
PageResult<RoleVO> page(RolePageQuery query);
RoleVO getVOById(Long id);
List<RoleVO> listAll();
}实现类
java
@Service
@RequiredArgsConstructor
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
private final RoleMenuMapper roleMenuMapper;
private final UserRoleMapper userRoleMapper;
private final RoleConvert roleConvert;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createRole(RoleCreateDTO dto) {
// ① 编码唯一性
if (baseMapper.selectByCode(dto.getCode()) != null) {
throw new BusinessException(ErrorCode.ROLE_EXISTS);
}
Role role = roleConvert.toEntity(dto);
role.setStatus(1);
baseMapper.insert(role);
// ② 分配菜单
if (dto.getMenuIds() != null && !dto.getMenuIds().isEmpty()) {
assignMenus(role.getId(), dto.getMenuIds());
}
return role.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateRole(Long id, RoleUpdateDTO dto) {
Role role = baseMapper.selectById(id);
Assert.notNull(role, "角色不存在");
roleConvert.updateEntity(dto, role);
baseMapper.updateById(role);
if (dto.getMenuIds() != null) {
assignMenus(id, dto.getMenuIds());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteRole(Long id) {
// ① 保护超管
Assert.isTrue(!id.equals(1L), "超级管理员角色不可删除");
// ② 检查是否被用户使用
Long userCount = userRoleMapper.countByRoleId(id);
if (userCount > 0) {
throw new BusinessException(ErrorCode.ROLE_IN_USE);
}
baseMapper.deleteById(id);
roleMenuMapper.deleteByRoleId(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void assignMenus(Long roleId, List<Long> menuIds) {
// 先删后插(简单可靠)
roleMenuMapper.deleteByRoleId(roleId);
if (menuIds != null && !menuIds.isEmpty()) {
for (Long menuId : menuIds) {
roleMenuMapper.insert(new RoleMenu(roleId, menuId));
}
}
// ① 失效该角色所有用户的权限缓存
List<Long> userIds = userRoleMapper.selectUserIdsByRoleId(roleId);
for (Long userId : userIds) {
permissionService.clearCache(userId);
}
}
@Override
public PageResult<RoleVO> page(RolePageQuery query) {
Page<Role> page = new Page<>(query.getCurrent(), query.getSize());
LambdaQueryWrapper<Role> wrapper = new LambdaQueryWrapper<Role>()
.like(StringUtils.hasText(query.getKeyword()), Role::getName, query.getKeyword())
.eq(query.getStatus() != null, Role::getStatus, query.getStatus())
.orderByAsc(Role::getSort);
IPage<Role> result = baseMapper.selectPage(page, wrapper);
return PageResult.of(result, roleConvert::toVO);
}
@Override
public List<RoleVO> listAll() {
return baseMapper.selectList(
new LambdaQueryWrapper<Role>()
.eq(Role::getStatus, 1)
.orderByAsc(Role::getSort))
.stream().map(roleConvert::toVO).toList();
}
}⚠️ 坑 2:改完角色菜单必须清权限缓存,否则用户继续用旧权限。生产事故常见原因:开发改完功能自测一切正常,运营同事反馈"我没改动怎么没生效",就是缓存清错了。
四、Controller
java
@RestController
@RequestMapping("/api/role")
@RequiredArgsConstructor
@Tag(name = "角色管理")
public class RoleController {
private final RoleService roleService;
@GetMapping("/page")
@Operation(summary = "角色分页")
public Result<PageResult<RoleVO>> page(RolePageQuery query) {
return Result.ok(roleService.page(query));
}
@GetMapping("/all")
@Operation(summary = "所有启用的角色")
public Result<List<RoleVO>> all() {
return Result.ok(roleService.listAll());
}
@GetMapping("/{id}")
@Operation(summary = "角色详情(含菜单ID)")
public Result<RoleVO> getById(@PathVariable Long id) {
return Result.ok(roleService.getVOById(id));
}
@PostMapping
@PreAuthorize("hasAuthority('role:create')")
@Operation(summary = "创建角色")
public Result<Long> create(@RequestBody @Valid RoleCreateDTO dto) {
return Result.ok(roleService.createRole(dto));
}
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('role:update')")
public Result<Void> update(@PathVariable Long id,
@RequestBody @Valid RoleUpdateDTO dto) {
roleService.updateRole(id, dto);
return Result.ok();
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('role:delete')")
public Result<Void> delete(@PathVariable Long id) {
roleService.deleteRole(id);
return Result.ok();
}
@PutMapping("/{id}/menus")
@PreAuthorize("hasAuthority('role:assign')")
@Operation(summary = "分配菜单")
public Result<Void> assignMenus(@PathVariable Long id,
@RequestBody List<Long> menuIds) {
roleService.assignMenus(id, menuIds);
return Result.ok();
}
}五、用户分配角色
角色管理之外,还需要"给用户分配角色"接口(在用户模块里):
java
@PutMapping("/{id}/roles")
@PreAuthorize("hasAuthority('user:assign-role')")
@Operation(summary = "分配角色")
public Result<Void> assignRoles(@PathVariable Long id,
@RequestBody List<Long> roleIds) {
userService.assignRoles(id, roleIds);
return Result.ok();
}java
@Override
@Transactional
public void assignRoles(Long userId, List<Long> roleIds) {
userRoleMapper.deleteByUserId(userId);
if (roleIds != null) {
for (Long roleId : roleIds) {
userRoleMapper.insert(new UserRole(userId, roleId));
}
}
// 失效该用户的权限缓存
permissionService.clearCache(userId);
}六、批量校验:一个用户多个角色
角色数据合并时,典型的坑:
java
// 用户 A 有两个角色:ROLE_USER + ROLE_VIEWER
// 当前用户的所有权限是这两个角色菜单的并集
public Set<String> loadFromDB(Long userId) {
Set<String> permissions = new HashSet<>();
// ① 查用户的所有角色
List<Role> roles = roleMapper.selectByUserId(userId);
// ② 取所有角色绑定的菜单,合并按钮权限
for (Role role : roles) {
List<Menu> menus = menuMapper.selectByRoleId(role.getId());
for (Menu menu : menus) {
if (menu.getType() == 3 && StringUtils.hasText(menu.getPermission())) {
permissions.add(menu.getPermission());
}
}
}
return permissions;
}⚠️ 坑 3:权限合并必须用
Set,不能用List!否则重复的permission会导致hasAuthority重复判定,本身没问题但浪费内存。同一个用户的多个角色有重复菜单很常见。
七、前端展示(分配菜单对话框)
用 Element Plus 的 <el-tree> 和 <el-checkbox>:
vue
<template>
<el-dialog v-model="visible" title="分配菜单" width="600px">
<el-tree
ref="treeRef"
:data="menuTree"
:props="{ label: 'name', children: 'children' }"
show-checkbox
node-key="id"
:default-checked-keys="checkedIds"
/>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="submit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import api from '@/api'
const props = defineProps({ roleId: Number })
const visible = ref(false)
const menuTree = ref([])
const checkedIds = ref([])
const treeRef = ref()
watch(visible, async (val) => {
if (val) {
const { data } = await api.getMenuTree()
menuTree.value = data
const detail = await api.getRoleById(props.roleId)
checkedIds.value = detail.data.menuIds
}
})
const submit = async () => {
const ids = treeRef.value.getCheckedKeys()
.concat(treeRef.value.getHalfCheckedKeys()) // 父节点也带上
await api.assignMenus(props.roleId, ids)
visible.value = false
}
</script>八、本章小结
| 要点 | 关键 |
|---|---|
| 角色编码 | ROLE_ 前缀,大写字母下划线 |
| 数据权限 | dataScope 1-5,5 配合 sys_role_dept |
| 删除保护 | 校验是否被用户使用 |
| 菜单绑定 | 先删后插,失效权限缓存 |
| 多角色并集 | 用 Set 收集 permission |
动手练习
- 创建角色:实现新增/修改/删除角色接口,带菜单分配
- 测权限失效:创建角色 → 分配菜单 → 用该角色用户登录 → 看权限生效
- 删除保护:测试删除"超级管理员"被拦,删除"被用户引用的角色"被拦
下一章:第 9 章:菜单模块 →