nestjs + typeorm + mysql 实现用户权限管理应用 (一)

325 阅读3分钟

项目初衷

统一管理不同应用之间 用户、角色、权限管理。减少后续多应用开发权限管理问题。

创建项目并初始化

npm i -g @nestjs/cli
nest new user-app

连接 mysql 数据库

使用 @nestjs/config 包管理数据库相关配置信息

npm i --save @nestjs/config

根目录下创建 .env 文件 写入相关配置

安装连接数据库 相关依赖

npm install --save @nestjs/typeorm typeorm mysql2

相关代码

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService, ConfigModule } from '@nestjs/config';
import envConfig from '../config/env';

@Module({
  imports: [
     // 项目配置文件
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: [envConfig.path],
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => {
        return {
          type: 'mysql', // 数据库类型
          autoLoadEntities: true, // 
          host: configService.get('DB_HOST', localhost), // 主机,默认为localhost
          port: configService.get<number>('DB_PORT', 3306), // 端口号
          username: configService.get('DB_USER', 'root'), // 用户名
          password: configService.get('DB_PASSWORD', '123456'), // 密码
          database: configService.get('DB_DATABASE', 'user_system'), //数据库名
          timezone: '+08:00', //服务器上配置的时区
          synchronize: true, //根据实体自动创建数据库表,修改字段类型时会清除原有数据, 生产环境建议关闭
        };
      },
    }),
  ],
})
export class AppModule {}

注意: synchronize: true, 设置为 true时 改变 修改实体时对应的数据库表也会被修改 。

# 会根据 实体结构以及关系自动创建表
autoLoadEntities: true,
synchronize: true,

业务开发

数据库表 ER 图

image.png

应用权限相关接口开发示例

一般情况下 创建一个资源包含 如下几个文件。然后编写CRUD 代码

image.png

permissions.controller.ts

import { Controller, Get, Post, Body, Query, UseGuards } from '@nestjs/common';
import { PermissionsService } from './permissions.service';

import {
  CreatePermissionDto,
  UpdatePermissionDto,
  SearchPermissionDto,
} from './dto/permission';

import { IsNotEmpty} from 'class-validator';

@Controller('permission')
export class PermissionsController {
  constructor(private readonly permissionsService: PermissionsService) {}


  @Post('create')
  create(@Body() createPermissionDto: CreatePermissionDto) {
    return this.permissionsService.create(createPermissionDto);
  }

  @Get('list')
  findAll() {
    return this.permissionsService.list();
  }

  @Post('page')
  getPage(@Body() body: SearchPermissionDto) {
    return this.permissionsService.getPage(body);
  }

  @Post('update')
  update(@Body() updatePermissionDto: UpdatePermissionDto) {
    return this.permissionsService.update(updatePermissionDto);
  }

  @Get('detail')
  @IsNotEmpty()
  findOne(@Query('id') id: string) {
    return this.permissionsService.findOne(+id);
  }

   
  @Get('remove')
  @IsNotEmpty()
  remove(@Query('id') id: string) {
    return this.permissionsService.remove(+id);
  }
}

permissions.service.ts


import {
  Injectable,
  HttpException,
  HttpStatus,
  Inject,
  forwardRef,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import {
  CreatePermissionDto,
  UpdatePermissionDto,
  SearchPermissionDto,
} from './dto/permission';
import Permission from './entities/permission.entity';
import { RolesService } from 'src/roles/roles.service';
import Role from 'src/roles/entities/role.entity';

@Injectable()
export class PermissionsService {
  constructor(
    @InjectRepository(Permission)
    private readonly permissionRepository: Repository<Permission>,
    //  Inject  forwardRef 解决循环依赖问题
    @Inject(forwardRef(() => RolesService))
    private rolesService: RolesService,
  ) {}

  async getSubmitDto(data: CreatePermissionDto | UpdatePermissionDto) {
    const { roleIds = [], ...reset } = data;

    let roles: Role[] = [];

    if (roleIds?.length > 0) {
      roles = await this.rolesService.findByIds(roleIds);
    }

    const param = {
      ...reset,
      roles,
    };
    return param;
  }

  /** 创建权限 */
  async create(createPermissionDto: CreatePermissionDto) {
    const { name } = createPermissionDto;
    const existPermission = await this.permissionRepository.findOne({
      where: { name },
    });

    if (existPermission) {
      throw new HttpException('角色已存在', HttpStatus.BAD_REQUEST);
    }

    const param = await this.getSubmitDto(createPermissionDto);

    const newPermission = this.permissionRepository.create(param);
    return await this.permissionRepository.save(newPermission);
  }

  /** 查询 */
  async list() {
    const qb = this.permissionRepository.createQueryBuilder('permissions');
    const result = await qb.getMany();
    return result;
  }


  /** 分页查询 */
  async getPage(body: SearchPermissionDto) {
    const { pageSize = 10, pageIndex = 1 } = body ?? {};
    const qb = this.permissionRepository.createQueryBuilder('permissions');

    qb.skip(pageSize * (pageIndex - 1)).take(pageSize);

    const total = await qb.getCount();
    const result = await qb.getMany();
    return { total, list: result };
  }

  async findOne(id: number) {
    const qb = this.permissionRepository.createQueryBuilder('permissions');
    qb.where('permissions.permission_id=:id').setParameter('id', id);

    const result = await qb.getOne();

    if (!result) {
      throw new HttpException('权限字符不存在', HttpStatus.BAD_REQUEST);
    }

    return result;
  }

  async update(updatePermissionDto: UpdatePermissionDto) {
    const exitsPermission = await this.permissionRepository.findOne({
      where: { permission_id: updatePermissionDto?.permission_id },
    });

    if (!exitsPermission) {
      throw new HttpException('权限字符不存在', HttpStatus.BAD_REQUEST);
    }

    const param = await this.getSubmitDto(updatePermissionDto);

    const newPermission = this.permissionRepository.merge(
      exitsPermission,
      param,
    );

    return await this.permissionRepository.save(newPermission);
  }

  async remove(id: number) {
    const exitsPermission = await this.permissionRepository.findOne({
      where: { permission_id: id },
    });

    if (!exitsPermission) {
      throw new HttpException('权限字符不存在', HttpStatus.BAD_REQUEST);
    }
    return await this.permissionRepository.remove(exitsPermission);
  }

  async findByIds(ids: number[]) {
    return this.permissionRepository.findBy({ permission_id: In(ids) });
  }
}

permissions.module.ts

import { Module, forwardRef } from '@nestjs/common';
import { PermissionsService } from './permissions.service';
import { PermissionsController } from './permissions.controller';

import Permission from './entities/permission.entity';
import { TypeOrmModule } from '@nestjs/typeorm';

import { RolesModule } from 'src/roles/roles.module';

@Module({
  imports: [
    TypeOrmModule.forFeature([Permission]),
    forwardRef(() => RolesModule),
  ],
  controllers: [PermissionsController],
  providers: [PermissionsService],
  // 暴露 service  用处:假如role 中查询权限信息需要使用 PermissionsService查询数据
  exports: [PermissionsService],
})
export class PermissionsModule {}

entities/permission.entity.ts

权限 实体

import {} from '@nestjs/typeorm';
import { Column, Entity, PrimaryGeneratedColumn, ManyToMany } from 'typeorm';
import Role from 'src/roles/entities/role.entity';

@Entity('permissions')
export default class Permission {
  @PrimaryGeneratedColumn()
  permission_id: number;

  @Column()
  name: string;

  @Column()
  description: string;
    
  // 多对多关系处理 
  @ManyToMany(() => Role, (role) => role?.permissions)
  roles: Role[];
}

dto/permission.ts 接口说明

import { ApiProperty } from '@nestjs/swagger';
import { PartialType } from '@nestjs/mapped-types';
import { IsNotEmpty, IsNumber, IsOptional ,IsArray, MaxLength} from 'class-validator';

export class CreatePermissionDto {
  @ApiProperty()
  @IsNotEmpty()
  name: string;

  @ApiProperty()
  @IsOptional()
  @MaxLength(100)
  description: string;

  @ApiProperty()
  @IsOptional()
  @IsArray({each: true})
  roleIds: number[];
}

export class SearchPermissionDto extends PartialType(CreatePermissionDto) {
  @ApiProperty()
  @IsNotEmpty()
  @IsNumber()
  pageIndex: number;

  @ApiProperty()
  @IsNotEmpty()
  @IsNumber()
  pageSize: number;
}

export class UpdatePermissionDto extends PartialType(CreatePermissionDto) {
  @ApiProperty()
  @IsNotEmpty()
  @IsNumber()
  permission_id: number;
}

出现的问题

1.循环引入问题

在表关联关系中处理 多对多关系 会出现循环引入问题。官方给出处理方法

docs.nestjs.com/fundamental…

2. 表关联关系 多对多关系注意点

image.png

www.typeorm.org/many-to-man…

下一篇 将会介绍 接入swagger ,以及全局响应拦截器、验证管道。。。

相关链接

源码地址

nestjs 中文文档

nestjs 英文文档

typeorm 中文文档

typeorm 英文文档