目录

21.服务端实战用户服务开发下

基于上一章的用户鉴权部分之后,本章节将介绍主要的用户系统的开发主要细节(包含前后端一起),由于篇幅有限,整体的代码量非常的多,所以并不能将所有的功能都全部复制进来,只能尽可能的介绍一些关键点,同时与前端不同的是,当服务端的整体架构设计结束之后,基本的都是业务模块的 CURD 所以观看本章的时候会较为枯燥,有能力的同学可以掠过本章直接自己开发,有兴趣的同学建议拉完本章的实际项目对比来看效果更佳。

业务代码开发

前置条件

如果想体验丝滑的开发体验感觉,可以使用 Nginx 代理去掉端口号,配置如下:


    server {
        listen 80;
        server_name www.ig-space.com;
        location / {
        proxy_pass   http://127.0.0.1:10010/;
        }
    }

    server {
        listen 80;
        server_name api.ig-space.com;
        location / {
        proxy_pass   http://127.0.0.1:4000/;
        }
    }

其中 www.ig-space.com 对应前端应用,api.ig-space.com 对应服务端域名

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f9fd03107a6497f86d7bee4f4774613~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2716&h=1147&s=306428&e=png&b=061f28

将上一章 github 授权的链接替换该图标的链接,然后可以进入授权界面。

根据拿回的 code 调用授权三方的接口 http://api.ig-space.com/api/auth?code=44fbc2070464ff2abda3 就可以正常拿到用户接口并且登录。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/392b53d5f548466cbf2358af79678632~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2242&h=351&s=105214&e=png&b=fafaff

正常完成用户 jwt 注册之后可以看到如下所示,我们已经正常登录成功了。

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f06eb5258103481ea3dac7bd51979cbf~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=3838&h=582&s=92781&e=png&b=ffffff

这一块的前端后逻辑我还没有修改通顺,后期完成前后端的所有链路之后就可以自动授权跳转,目前还需要自己手动调用服务端登录一次。

接下来我们安装 RBAC 的权限模块体系来逐步讲解对应的前端端开发过程。

用户管理

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a6f1be976bc94b4f9b3cc9d8cda5a351~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2142&h=634&s=64181&e=png&b=fefefe

用户管理模块在上一章介绍的比较多,对于此系统目前来说,用户都来源于 Github 授权的能力,所以就只有一张表,但如果需要做个人用户登录的功能的话,则需要拓展三方用户信息表来保证用户主表的唯一性。

当然整个系统的功能非常庞大,非阻塞不重要的模块,我们后置处理,所以目前用户的不具备自主新增的功能,只接受 GitHub 授权添加,以及部分字段属性的修改功能。

系统管理

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/558f0b0d0ba04e9e8e5fd3c7c8e6a35c~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2182&h=660&s=84116&e=png&b=fdfdfd

用户系统的主要代码如下图所示区域:

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c4f080f9caa24b64af0274aca1126bdc~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2208&h=1653&s=343277&e=png&b=1c1c1c

实体类为:

import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

export enum STATUS {
  disabled = 0,
  enabled = 1,
}
@Entity()
export class System {
  @PrimaryGeneratedColumn()
  id?: number;

  @Column()
  name: string;

  @Column({ type: 'text', default: null })
  description?: string;

  @Column({ default: STATUS.enabled })
  status?: STATUS;

  @Column()
  creatorId?: number;

  @Column()
  creatorName?: string;

  @Column()
  updateId?: number;

  @Column()
  updateName?: string;

  @CreateDateColumn()
  createTime?: string;

  @UpdateDateColumn()
  updateTime?: string;
}

系统类的功能非常简单,主要帮助我们将权限限制再各个系统中,减少查询次数所以对应的 Controller 的功能也较为简洁,只有常规的 CURD 模块:

import { Body, Controller, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { BusinessException } from '@app/common';
import {
  CreateSystemDto,
  DeleteSystemDto,
  UpdateSystemDto,
} from './system.dto';
import { SystemService } from './system.service';
import { PayloadUser } from '@app/common';

@ApiTags('系统')
@Controller('system')
export class SystemController {
  constructor(private readonly systemService: SystemService) { }

  @ApiOperation({
    summary: '创建新系统',
  })
  @Post('create')
  create(@Body() dto: CreateSystemDto, @PayloadUser() user: Payload) {
    return this.systemService.create({
      ...dto,
      creatorName: user.name,
      creatorId: user.userId,
      updateName: user.name,
      updateId: user.userId,
    });
  }

  @ApiOperation({
    summary: '修改系统信息',
  })
  @Post('update')
  async update(@Body() dto: UpdateSystemDto, @PayloadUser() user: Payload) {
    const foundSystem = await this.systemService.findById(dto.id);

    if (!foundSystem) {
      throw new BusinessException('未找到系统');
    }

    return await this.systemService.update({
      ...foundSystem,
      ...dto,
      updateName: user.name,
      updateId: user.userId,
    });
  }

  @ApiOperation({
    summary: '删除系统',
  })
  @Post('/delete')
  async delete(@Body() dto: DeleteSystemDto) {
    return await this.systemService.delete(dto.id);
  }

  @ApiOperation({
    summary: '所有系统列表',
  })
  @Post('/list')
  async list() {
    return await this.systemService.list();
  }
}

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/14e011e3efb141ff90a2715b40b65028~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2850&h=699&s=114025&e=png&b=8b8b8b

资源管理

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9334470977ba4cd8b18a9bff4e34e725~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=3826&h=471&s=81258&e=png&b=fdfdfd

资源管理的服务端代码如下所示:

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa3d1d4d221342e89c142793192d1349~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1660&h=1396&s=277797&e=png&b=1c1c1c

当我们创建好系统模块之后就可以创建对应的资源模块,资源模块有两种形态:

  1. 菜单 -> 对应页面级别的可见模块主要用于前端展示模块
  2. 常规模块 -> 对应功能级别的模块主要用于服务端

同时每个资源都可能存在父子级别嵌套的功能,所以系统模块的实体类的设计相对于系统会较为复杂:

import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';
import { STATUS } from '../system/system.mysql.entity';

export enum ResourceType {
  Menu = 'menu',
  Nomal = 'nomal',
}

@Entity()
export class Resource {
  @PrimaryGeneratedColumn()
  id?: number;

  @Column()
  name: string;

  @Column()
  key: string; // 对应资源的可识别 key,并不等同于系统自建 id

  @Column({ default: 0 })
  sort?: number;  // 菜单类的资源才会有排序的功能

  @Column({ default: null })
  parentId?: number; // 父子嵌套,当为 null 为顶级资源

  @Column()
  systemId: number; // 归属于对应的系统

  @Column({ default: ResourceType.Nomal })
  type: ResourceType; // 资源类型

  @Column({ default: STATUS.enabled })
  status?: STATUS;

  @Column({ type: 'text', default: null })
  description?: string;

  @CreateDateColumn()
  createTime?: string;

  @UpdateDateColumn()
  updateTime?: string;
}

资源类的交互相对于系统来说会更多一下,毕竟涉及了下面的权限与角色模块:

import { Body, Controller, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { BusinessException } from '@app/common';
import {
  CreateResourceDto,
  DeleteResourceDto,
  ListBySystemIdDto,
  ListWithPaginationDto,
  UpdateResourceDto,
} from './resource.dto';
import { ResourceService } from './resource.service';
import { SystemService } from '../system/system.service';
import { PrivilegeService } from '../privilege/privilege.service';

@Controller('resource')
@ApiTags('资源')
export class ResourceController {
  constructor(
    private readonly resourceService: ResourceService,
    private readonly systemService: SystemService,
    private readonly privilegeService: PrivilegeService,
  ) { }

  @ApiOperation({
    summary: '创建新资源',
  })
  @Post('create')
  async create(@Body() dto: CreateResourceDto) {
    const foundResource = await this.resourceService.findByKey(dto.key);

    if (foundResource) {
      throw new BusinessException('资源 Key 已存在');
    }

    return await this.resourceService.create(dto);
  }

  @ApiOperation({
    summary: '修改资源信息',
  })
  @Post('update')
  async update(@Body() dto: UpdateResourceDto) {
    const foundResource = await this.resourceService.findById(dto.id);

    if (!foundResource) {
      throw new BusinessException('未找到资源');
    }
    const allowUpdateFields = {
      name: dto.name,
      description: dto.description,
    };

    return await this.resourceService.update({
      ...foundResource,
      ...allowUpdateFields,
    });
  }

  @ApiOperation({
    summary: '删除资源',
    description: '',
  })
  @Post('/delete')
  async delete(@Body() dto: DeleteResourceDto) {
    return await this.resourceService.delete(dto.id);
  }

  @ApiOperation({
    summary: '资源列表',
    description: '根据角色名称查询',
  })
  @Post('/list/paginate')
  async list(@Body() dto: ListWithPaginationDto) {
    const { page, ...searchParams } = dto;
    const rourceData = await this.resourceService.paginate(searchParams, page);
    const systemIds = rourceData.items.map((role) => role.systemId);
    const systemList = await this.systemService.findByIds(systemIds);
    const systemMap = {};
    systemList.forEach((system) => (systemMap[system.id] = system));
    const newRource = rourceData.items.map((role) => {
      role['systemName'] = systemMap[role.systemId].name;
      return role;
    });
    return { ...rourceData, items: newRource };
  }

  @ApiOperation({
    summary: '资源列表',
    description: '根据系统 id 查询',
  })
  @Post('/listBySystemId')
  async listBySystemId(@Body() dto: ListBySystemIdDto) {
    const resourceList = await this.resourceService.listBySystemId(
      dto.systemId,
    );
    const newResource = [];

    for (const resource of resourceList) {
      const privileges = await this.privilegeService.listByResourceKey(
        resource.key,
      );
      newResource.push({
        ...resource,
        privileges,
      });
    }
    return newResource;
  }
}

权限管理

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eae3b0975e4b449eb558631c2b853d2b~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1786&h=1023&s=97180&e=png&b=fefefe

权限的服务端代码集中在下图所示:

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f7435d7abef04fce9a807df7fa6a47f9~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=783&h=745&s=63783&e=png&b=191919

在新建资源之后就是对应资源下的具体权限管理,可以理解为某个页面下的按钮级别权限。

权限主要是对应的描述,所以它的实体类也并不会很复杂:

import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
} from 'typeorm';

export enum PrivilegeStatus {
  DENY = 0,
  ALLOW = 1,
  NOT_SET = 2,
}

export enum Action {
  Manage = 'manage',
  Create = 'create',
  Read = 'read',
  Update = 'update',
  Delete = 'delete',
}

@Entity()
export class Privilege {
  @PrimaryGeneratedColumn()
  id?: number;

  @Column({ default: null })
  systemId?: number;

  @Column()
  resourceKey: string;

  @Column()
  name: string;

  @Column({ type: 'text', default: null })
  description?: string;

  @Column()
  action: Action;

  @Column({ default: PrivilegeStatus.ALLOW })
  status?: PrivilegeStatus;

  @CreateDateColumn()
  createTime?: string;
}

同样对应的 Controller 也较为简单:

import { Body, Controller, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { BusinessException } from '@app/common';
import { SystemService } from '../system/system.service';
import { ResourceService } from '../resource/resource.service';
import {
  CreatePrivilegeDto,
  DeletePrivilegeDto,
  DisablePrivilegeDto,
  ListAllPrivilegeDto,
  PrivilegeListWithPaginationDto,
  UpdatePrivilegeDto,
} from './privilege.dto';
import { Privilege } from './privilege.mysql.entity';
import { PrivilegeService } from './privilege.service';

@ApiTags('权限')
@Controller('privilege')
export class PrivilegeController {
  constructor(
    private readonly privilegeService: PrivilegeService,
    private readonly resourceService: ResourceService,
    private readonly systemService: SystemService,
  ) { }

  @ApiOperation({
    summary: '创建权限',
  })
  @Post('create')
  async create(@Body() dto: CreatePrivilegeDto) {
    const privilege: Privilege = {
      systemId: dto.systemId,
      name: dto.name,
      resourceKey: dto.resourceKey,
      action: dto.action,
      description: dto.description,
    };
    const resource = await this.resourceService.findByKey(dto.resourceKey);
    if (!resource) {
      throw new BusinessException('未找到资源 Key:' + dto.resourceKey);
    }
    return this.privilegeService.createOrUpdate(privilege);
  }

  @ApiOperation({
    summary: '修改权限',
  })
  @Post('update')
  async update(@Body() dto: UpdatePrivilegeDto) {
    const updatedPrivilege: Privilege = {
      name: dto.name,
      systemId: dto.systemId,
      resourceKey: dto.resourceKey,
      action: dto.action,
      description: dto.description,
    };

    const privilege = await this.privilegeService.findById(dto.id);

    if (!privilege) {
      throw new BusinessException(`未找到 id 为 ${dto.id} 的权限`);
    }

    const resource = await this.resourceService.findByKey(dto.resourceKey);
    if (!resource) {
      throw new BusinessException('未找到资源 Key:' + dto.resourceKey);
    }

    return this.privilegeService.createOrUpdate({
      ...privilege,
      ...updatedPrivilege,
    });
  }

  @ApiOperation({
    summary: '是否冻结权限',
  })
  @Post('changeStatus')
  async changeStatus(@Body() dto: DisablePrivilegeDto) {
    const found = await this.privilegeService.findById(dto.privilegeId);
    if (!found) {
      throw new BusinessException(`未找到 ID 为 ${dto.privilegeId} 的权限`);
    }
    return this.privilegeService.createOrUpdate({
      ...found,
      status: dto.status,
    });
  }

  @ApiOperation({
    summary: '删除权限',
  })
  @Post('delete')
  async delete(@Body() dto: DeletePrivilegeDto) {
    return this.privilegeService.delete(dto.privilegeId);
  }

  @ApiOperation({
    summary: '权限列表(分页)',
    description: '根据权限名称查询',
  })
  @Post('/list/pagination')
  async listWithPagination(@Body() dto: PrivilegeListWithPaginationDto) {
    const { page, ...searchParams } = dto;

    const pageData = await this.privilegeService.paginate(searchParams, page);
    const systemIds = pageData.items.map((privilege) => privilege.systemId);
    const systemList = await this.systemService.findByIds(systemIds);
    const systemMap = {};
    systemList.forEach((system) => (systemMap[system.id] = system));
    const newRoles = pageData.items.map((privilege) => {
      privilege['systemName'] = systemMap[privilege.systemId].name;
      return privilege;
    });
    return { ...pageData, items: newRoles };
  }

  @ApiOperation({
    summary: '获取所有权限',
  })
  @Post('listBySys')
  async list(@Body() dto: ListAllPrivilegeDto) {
    return await this.privilegeService.list(dto.systemId);
  }
}

角色管理

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/90ef7cb83e584c88897cc96c96100a16~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2172&h=1080&s=149923&e=png&b=fefefe

这一块是最为复杂的一点,因为角色需要同时关联用户以及权限,所以整体的设计比较复杂,为了将系统的拓展性做的比较通用,我们采用的是 role-privilege 以及 role-user 关联表的设计。

角色管理的服务端代码如下:

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8deee12e8d104365958fe0c888c0d6b5~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=886&h=378&s=34191&e=png&b=1a1a1a

所以对应的实体类有 3 张表:

// role
import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';
import { STATUS } from '../system/system.mysql.entity';

@Entity()
export class Role {
  @PrimaryGeneratedColumn()
  id?: number;
  @Column()
  name: string;

  @Column()
  systemId: number;

  @Column()
  systemName: string;

  @Column()
  creatorId?: number;

  @Column()
  creatorName?: string;

  @Column()
  updateId?: number;

  @Column()
  updateName?: string;

  @Column({ default: STATUS.enabled })
  status?: STATUS;

  @Column({ type: 'text', default: null })
  description?: string;

  @CreateDateColumn()
  createTime?: string;

  @UpdateDateColumn()
  updateTime?: string;
}
// role-user
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class UserRole {
  @PrimaryGeneratedColumn()
  id?: number;

  @Column({ default: null })
  systemId?: number;

  @Column()
  userId: number;

  @Column()
  roleId: number;
}
// role-privilege
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9da67d03e95446f7b79f0343fe16bfc6~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2130&h=762&s=131105&e=png&b=8b8b8b)
@Entity()
export class RolePrivilege {
  @PrimaryGeneratedColumn()
  id?: number;

  @Column({ default: null })
  systemId?: number;

  @Column()
  roleId: number;

  @Column()
  privilegeId: number;
}

在新增角色的时候就需要根据系统来确定角色的归属:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1ee33bed1cbb49a6863079f2fecfb5e8~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2172&h=1080&s=173357&e=png&b=8b8b8b

所以在用户授权权限的时候,其实是新增 role-privilege 表数据:

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cdc3b3b220d443c7b051f3acfd12e704~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2172&h=1080&s=163476&e=png&b=8b8b8b

再完成角色授权之后可以进行用户的角色给予:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/37ec3fba264144ea8c633cb409c54ec5~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2020&h=862&s=107501&e=png&b=8b8b8b

同样这里的创建关系也是新增 role-user 表数据。

所以综合来看角色的 Controller 相对于之前会较为复杂点:

import { Body, Controller, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { BusinessException, PayloadUser } from '@app/common';
import { PrivilegeService } from '../privilege/privilege.service';
import { RolePrivilegeService } from '../role-privilege/role-privilege.service';
import { SystemService } from '../system/system.service';
import {
  CreateRoleDto,
  DeleteRoleDto,
  GetPrivilegeListByIdDto,
  RoleListDto,
  RoleListWithPaginationDto,
  RolePrivilegeSetDto,
  UpdateRoleDto,
} from './role.dto';
import { RoleService } from './role.service';

@Controller('role')
@ApiTags('角色')
export class RoleController {
  constructor(
    private readonly roleService: RoleService,
    private readonly rolePrivilegeService: RolePrivilegeService,
    private readonly privilegeService: PrivilegeService,
    private readonly systemService: SystemService,
  ) { }

  @ApiOperation({
    summary: '创建新角色',
  })
  @Post('create')
  async create(
    @Body() createRoleDto: CreateRoleDto,
    @PayloadUser() user: Payload,
  ) {
    const system = await this.systemService.findById(createRoleDto.systemId);
    return this.roleService.create({
      ...createRoleDto,
      systemName: system.name,
      creatorName: user.name,
      creatorId: user.userId,
      updateName: user.name,
      updateId: user.userId,
    });
  }

  @ApiOperation({
    summary: '修改角色信息',
  })
  @Post('update')
  async update(@Body() dto: UpdateRoleDto, @PayloadUser() user: Payload) {
    const foundRole = await this.roleService.findById(dto.id);
    if (!foundRole) {
      throw new BusinessException('未找到角色');
    }
    return await this.roleService.update({
      ...foundRole,
      ...dto,
      updateName: user.name,
      updateId: user.userId,
    });
  }

  @ApiOperation({
    summary: '删除角色',
    description:
      '如果发现角色有绑定权限,权限将同步删除 Role - privilege 关系表',
  })
  @Post('/delete')
  async delete(@Body() dto: DeleteRoleDto) {
    return await this.roleService.delete(dto.id);
  }

  @ApiOperation({
    summary: '角色 ID 查权限',
    description: '根据角色 id 查权限列表',
  })
  @Post('/getPrivilegeListById')
  async getPrivilegeListById(@Body() dto: GetPrivilegeListByIdDto) {
    const rolePrivilegeList = await this.rolePrivilegeService.listByRoleIds([
      dto.roleId,
    ]);
    const privilegeList = await this.privilegeService.findByIds(
      rolePrivilegeList.map((rp) => rp.privilegeId),
    );
    return privilegeList;
  }

  @ApiOperation({
    summary: '角色列表(分页)',
    description: '根据角色名称查询',
  })
  @Post('/list/pagination')
  async listWithPagination(@Body() dto: RoleListWithPaginationDto) {
    const { page, ...searchParams } = dto;
    const pageData = await this.roleService.paginate(searchParams, page);
    const systemIds = pageData.items.map((role) => role.systemId);
    const systemList = await this.systemService.findByIds(systemIds);
    const systemMap = {};
    systemList.forEach((system) => (systemMap[system.id] = system));
    const newRoles = pageData.items.map((role) => {
      role['systemName'] = systemMap[role.systemId].name;
      return role;
    });
    return { ...pageData, items: newRoles };
  }

  @ApiOperation({
    summary: 'tree 形状角色列表',
    description: '系统级别树状',
  })
  @Post('/list/withSystem')
  async listWithSys() {
    const newSys = [];
    const systemList = await this.systemService.list();
    for (const sys of systemList) {
      const roles = await this.roleService.listWithSys(sys.id);
      newSys.push({
        ...sys,
        roles,
      });
    }
    return newSys;
  }

  @ApiOperation({
    summary: '角色分配权限',
    description: '',
  })
  @Post('set')
  async set(@Body() dto: RolePrivilegeSetDto) {
    await this.rolePrivilegeService.remove(dto.roleId);
    return await this.rolePrivilegeService.set(
      dto.roleId,
      dto.privilegeIds,
      dto.systemId,
    );
  }
}

写在最后

为什么只粘贴 Controller

因为 Service 职责比较单一,在用户系统里面并没有过多的复杂操作,本身就是对权限数据的增删改查,每个 Service 都比较类似就不放上到文章中了,而 Controller 有些部分是需要适配业务也有多表关联查询数据,所以就粘贴了 Controller 层的代码。

为什么不使用多对多的关系

关联表的设计比较复杂,代码写的比较麻烦,也未必是最好的选择,所以就抛弃了关联表设计,代码会比较傻瓜式的开发,而且从性能上,一般的服务也足够使用,并不需要考虑太多。

为什么数据库表要这么设计?

在最开始的 RBAC 用户权限设计中就已经介绍过了,这里再提一下现实中的常规场景,一般用户在登陆系统的时候是可以感知到对应的登陆系统的,所以此时可以通过系统以及用户来获取到对应的角色,在通过角色拿到对应的权限此时的路径是最为简便的,所以将系统作为用户的直接属性,而用户与权限则作为关联表存在。

为什么本章的代码含量居多

如开头所言,服务端开发反而不是最难的事情,难在架构设计、CICD、分布式、数据一致性等等额外的模块上,所以通常服务端的业务设计以及基础架构花费很多的时间,而代码开发方面占比并不高。

当我们将服务端的设计全部讲完,并且已经熟悉了 NestJS 的开发之后,理论上就可以开始服务端的开发,这也是为什么之前的 NestJS 的实战小册介绍完设计以及 NestJS 的开发之后就可以完结的原因。

因为业务代码的开发是重复且枯燥的,而其他有意思的内容对前端来说学起来又会很吃力,所以在这本大而全的体系中,才方便将所有的代码细节以及服务端的其他模块都串联起来。

下一章是物料服务的前后端开发,加油!狗狗狗!

如果你有什么疑问,欢迎在评论区提出或者加群沟通。 👏