nestjs实战-权限二:角色模块

用户 (User) → 角色 (Role) → 菜单/权限 (Menu/Permission)

一、权限需要哪些字段

基础字段:

  • id
  • name
  • status
  • remark

关联字段:

  • menu
  • user

基础字段的 CRUD 不过多介绍,主要了解一下 中间表、关联字段; 先看看两个实体中如何定义关系的:

二、创建角色

menu.entity.ts

typescript 复制代码
import { Column, Entity, Index, ManyToMany, ManyToOne, OneToMany, Relation } from 'typeorm'
import { CommonEntity } from '~/common/entity/common.entity'
import { RoleEntity } from '../../role/entities/role.entity'
import { MenuType } from '../dto/menu.dto'
​
@Entity('sys_menu')
export class MenuEntity extends CommonEntity {
  // ...
​
  @ManyToMany(() => RoleEntity, role => role.menus, {
    onDelete: 'CASCADE',
  })
  roles: Relation<RoleEntity[]>
}

role.entity.ts

less 复制代码
import { Column, Entity, Index, ManyToMany, JoinTable, Relation } from 'typeorm'
import { CommonEntity } from '~/common/entity/common.entity'
import { MenuEntity } from '../../menu/entities/menu.entity'
import { UserEntity } from '~/modules/users/entities/user.entity'
​
@Entity('sys_role')
export class RoleEntity extends CommonEntity {
  // ...
  
  @ManyToMany(() => MenuEntity, menu => menu.roles, {})
  @JoinTable({
    name: 'sys_role_menus',
    joinColumn: { name: 'role_id', referencedColumnName: 'id' },
    inverseJoinColumn: { name: 'menu_id', referencedColumnName: 'id' },
  })
  menus: Relation<MenuEntity[]>
}

role实体中 menus 字段上有 @JoinTable,所以它是关系拥有方。

疑问

一、在创建角色时,如何更新中间表?

先看一下创建角色的实现代码:

kotlin 复制代码
/**
 * 创建角色
 * - `code` / `name` 任一重复都拒绝创建(对应实体唯一索引)
 */
async create({ menuIds, ...data }: CreateRoleDto) {
  const exists = await this.roleRepository.findOne({
    where: [
      { code: data.code },
      { name: data.name },
    ],
  })
  if (exists)
    throw new HttpException('角色编码或名称已存在', 400)
​
  const entity = this.roleRepository.create({
    ...data,
  })
​
  if (menuIds?.length) {
    const menus = await this.menuRepository.findBy({ id: In(menuIds) })
    if (menus.length !== menuIds.length)
      throw new HttpException('部分菜单不存在', 400)
    entity.menus = menus
  }
​
  const role = await this.roleRepository.save(entity)
​
  return { roleId: role.id }
}

创建角色,需要传递角色的基本信息菜单ids,然后通过 menuIds 去数据库中查处相关数据,组装成一个新的 role实体;

save方法将新建的 role实体 存入数据库中;

这个过程中:

  • 先创建 role
  • 然后获取 roleId,更新中间表sys_role_menus,将roleId 和 menuId 一一对应

所以:

save 时不能直接写 menus: menuIds(那是数字数组),需要 MenuEntity[]。所以先用 findBy 把 ID 转成实体,TypeORM 再在 sys_role_menus 里写入关联。

  • menuIds → 查出对应菜单并关联
  • 没有 menuIdsmenus: [],不关联任何菜单

自动更新中间表,不需要手动去维护!!

三、获取角色详情

  • 返回角色信息和关联菜单 ID 列表
  • 若角色不存在,则抛出 404 错误
  • 若角色存在,则返回角色信息和关联菜单 ID 列表
csharp 复制代码
async findOne(id: number) {
  const role = await this.roleRepository.findOne({ where: { id } })
  if (!role)
    throw new HttpException('角色不存在', 404)
​
  // 查出所有关联了该角色的菜单
  const menus = await this.menuRepository.find({
    where: { roles: { id } },
    select: ['id'],
  })
​
  return { ...role, menuIds: menus.map(m => m.id) }
}
  • this.menuRepository.find(...) 用 TypeORM 的 MenuEntity 仓库查 sys_menu 表,返回的是菜单对象数组。
  • where: { roles: { id } } 按关联关系过滤,不是按菜单自己的 id

rolesMenuEntity 上的多对多字段,对应中间表 `sys_role_menus``

{ id }{ id: id } 的简写,这里的 id 来自外层 findOne(id: number) 的参数,即角色 id

含义:查出所有关联了该角色的菜单。

  • select: ['id']

    只查 id 列,不查 titlepathcomponent 等。

    返回结果类似:

    bash 复制代码
    [
    ​
    { id: 1 },
    ​
    { id: 5 },
    ​
    { id: 8 },
    ​
    ]

疑问:

为什么在数据库中, sys_menu 表中并没有展示|看到 roles 字段?

因为 roles 不是 sys_menu 表里的真实列,而是 TypeORM 在代码里定义的关联属性,用来表示「这个菜单关联了哪些角色」。

实体 vs 数据库

MenuEntity 里:

kotlin 复制代码
@ManyToMany(() => RoleEntity, role => role.menus, {
​
    onDelete: 'CASCADE',
​
  })
​
  roles: Relation<RoleEntity[]>

这里的 roles 带有 @ManyToMany,表示多对多关系,不会映射成 sys_menu 上的一列。

你在数据库里看到的 sys_menu,通常只有菜单自己的字段,例如:

  • id
  • parent_id
  • title
  • path
  • component
  • ...

不会有 roles 列,这是正常的。

关系存在哪里?

角色和菜单是多对多,实际数据放在中间表里。你们项目里在 RoleEntity 一侧定义了:

role.entity.ts

less 复制代码
 @ManyToMany(() => MenuEntity, menu => menu.roles, {})
​
  @JoinTable({
​
    name: 'sys_role_menus',
​
    joinColumn: { name: 'role_id', referencedColumnName: 'id' },
​
    inverseJoinColumn: { name: 'menu_id', referencedColumnName: 'id' },
​
  })
​
  menus: Relation<MenuEntity[]>

也就是说:

作用
sys_menu 存菜单本身
sys_role 存角色本身
sys_role_menus 存「哪个角色 ↔ 哪个菜单」的对应关系

中间表大致长这样:

role_id menu_id
1 3
1 5
2 3

一条记录表示:角色 1 拥有菜单 3 的权限。

那代码里的 where: { roles: { id } } 是什么意思?

这是 TypeORM 的关联查询语法,不是去查 sys_menu.roles 这一列。

它会被翻译成类似这样的 SQL:

ini 复制代码
FROM sys_menu menu
​
INNER JOIN sys_role_menus rm ON rm.menu_id = menu.id
​
WHERE rm.role_id = :id

所以:

  • roles:代码里的关系名,方便 ORM 写查询、加载关联数据
  • sys_role_menus:数据库里真正存关系的地方

类比理解

可以把它想成:

  • sys_menu = 菜单清单
  • sys_role = 角色清单
  • sys_role_menus = 勾选表(谁勾选了哪些菜单)

勾选关系不会写进菜单表的一列里,而是单独一张关联表。


总结:在数据库里看不到 roles 字段是预期行为;要看角色和菜单的绑定关系,请查 sys_role_menus 表,而不是在 sys_menu 里找 roles 列。

更新角色

  • 传递参数:

    bash 复制代码
    params: {
      id,
      {
        menuIds,
        ...data
      }
    }
  • 通过id查询 角色是否存在,不存在返回 404

  • 通过id查询出 role 实体,通过data 更新角色信息

  • 通过munuIds 更新绑定的 menu 信息

  • 最后保存 新的 role

  • 建议:通过事务完成以上操作

csharp 复制代码
async update(id, { menuIds, ...data }: RoleUpdateDto): Promise<void> {
  const role = await this.roleRepository.findOne({ where: { id } })
  if (!role)
    throw new HttpException('角色不存在', 404)
​
  await this.roleRepository.update(id, data)
  await this.entityManager.transaction(async (manager) => {
    const role = await this.roleRepository.findOne({ where: { id } })
    role.menus = menuIds?.length
      ? await this.menuRepository.findBy({ id: In(menuIds) })
      : []
    await manager.save(role)
  })
}

删除角色

typescript 复制代码
async delete(id: number): Promise<void> {
  if (id === ROOT_ROLE_ID)       
    throw new Error('不能删除超级管理员')
  await this.roleRepository.delete(id)
}

roleRepository.delete(id) 实际做了什么

repository.delete(id) 只会执行类似:

sql 复制代码
DELETE FROM sys_role WHERE id = ?

它不会主动再发一条 DELETE FROM sys_role_menus ...

TypeORM 的 delete 只管主表,不会像 update 里改 role.menus 那样去维护多对多关系。

中间表 sys_role_menus 会不会被清空?

会。 项目建表时给 role_id 配了 ON DELETE CASCADE

sql 复制代码
CREATE TABLE `sys_role_menus` (
​
  `role_id` int NOT NULL,
​
  `menu_id` int NOT NULL,
​
  ...
​
  CONSTRAINT `FK_35ce749b04d57e226d059e0f633` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE

删除 sys_role 某行时,MySQL 会自动删除 sys_role_menus 里所有 role_id = 该 id 的记录。

注意:

  • 删的是关联记录,不是 sys_menu 里的菜单本身。
  • 若数据库没有这条 CASCADE(例如手工改过表结构),中间表可能残留脏数据。
相关推荐
默默且听风1 小时前
Ubuntu 22 环境下 VS Code Codex 插件无法打开的排查与修复记录
后端·ai编程·vibecoding
AskHarries1 小时前
权限模型:Shell、Browser、文件读写的安全边界
服务器·前端·网络
小蜜蜂dry1 小时前
nestjs实战-权限一: 菜单模块
前端·后端·nestjs
用户5812441541571 小时前
GemDesign MCP协议详解:从原型到代码的完整技术链路
前端
半个烧饼不加肉1 小时前
JS 底层探究-- 事件循环
开发语言·前端·javascript
goDeep2 小时前
useMemo 和 useCallback 的区别,我终于搞懂了
前端
小亮学前端2 小时前
在1Panel中部署Nuxt项目
前端·vue.js
产品研究员2 小时前
AI生成可用的React交互代码实测:Lovable vs Stitch vs Paico
前端·react.js·aigc
labixiong2 小时前
Prompt 工程:当一段文字学会了思考、行动与统治
前端·ai编程