Nestjs 实战系列(二)—— TypeORM

4,821 阅读16分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

TypeORM

TypeORM 是一个ORM框架,它可以运行在 NodeJS、Browser、Cordova、PhoneGap、Ionic、React Native、Expo 和 Electron 平台上,可以与 TypeScript 和 JavaScript (ES5,ES6,ES7,ES8)一起使用。 它的目标是始终支持最新的 JavaScript 特性并提供额外的特性以帮助你开发任何使用数据库的(不管是只有几张表的小型应用还是拥有多数据库的大型企业应用)应用程序。

TypeORM 参考了很多其他优秀 ORM 的实现, 比如 Hibernate, Doctrine 和 Entity Framework,其部分Api功能如下

详细请看官网地址: typeorm.bootcss.com/

安装与连接

npm i typeorm

注意如果连接不同的数据库也需要安装对应的数据库连接包,如本文用到的postgres,则需要

npm i pg -S

连接数据库

方式一: TypeOrmModule.forRoot

下面以连接postgresql为例

TypeOrm也支持Mongodb数据库,详细Api请看: typeorm.bootcss.com/mongodb

app.module

@Module({
  imports: [CoffeesModule, TypeOrmModule.forRoot(
    {
      type: 'postgres',
      host: '192.168.***.***',
      port: 5432,
      username: 'postgres',
      password: '******',
      database: '******',
      autoLoadEntities: true,
      synchronize: true
    }
  )],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }

synchronize可以让数据库根据实体类自动生成表及元数据(严禁在生产环境中使用,会破坏表结构

后面章节我们会通过读取配置的方式实现数据库配置


方式二: 也可以通过ormconfig.json 独立配置(不推荐)

在根目录下创建一个ormconfig.json文件(与src同级), 而不是将配置对象传递给forRoot()的方式。

然后在app.module.ts中不带任何选项的调用forRoot()

设置实例Entity

TypeOrm 可以使用两种模式, Active Record 和 Data Mapper 模式创建实例,并执行增删改查的操作。

创建实体类案例(Data Mapper 方式)

默认表名为class名称的小写,也可以通过设置@Entity(‘coffee’)来定义表名

import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

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

  @Column()
  name: string;

  @Column()
  brand: string;

  @Column()
  recommendations: number;

  @Column('json', { nullable: true })
  flavors: string[];
}

(3)module注册

coffee.module

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CoffeesController } from './coffees.controller';
import { CoffeesService } from './coffees.service';
import { Coffee } from './entities/coffee.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Coffee])],
  controllers: [CoffeesController],
  providers: [CoffeesService]
})
export class CoffeesModule { }

(4)Repository操作数据库

constructor(
    @InjectRepository(Coffee)
    private readonly coffeeRepository: Repository<Coffee>
){}

(5) 创建@OneToOne 与 @OneToMany方法

下面代码中userEntity与WorkItemEntity相互之间约定了@OneToMany与@ManyToOne实现相互关联,如果只是单向关联则可以不写其中一半(如只写了user - @OneToMany - workItem, 则只能通过UserEntity查询到userWorkItemInfo,从workItemEntity则无法查询到user信息了)。

注意: cascade 需要写在ManyToOne这一侧

// user.entity.ts
  @OneToMany(() => WorkItemEntity, (m) => m.userInfo)
  userWorkItemInfo: WorkItemEntity[];


// workItem.entity.ts
  @ManyToOne(() => UserEntity, (m) => m.userWorkItemInfo, {
    cascade: true,  // 注意cascade 需要写在ManyToOne这一侧
    onDelete: 'CASCADE',
    onUpdate: 'CASCADE',
  })
  @JoinColumn({ name: 'userId' })
  userInfo: UserEntity;

Active Record 在entity内定义静态方法

使用 Active Record 方法,可以在模型本身内定义所有查询方法,并使用模型方法保存、删除和加载对象。

下面是使用 Active Record 模式的样子:

  • 所有 active-record 实体都必须extends BaseEntity类,它提供了与实体一起使用的方法。
  • 可以在User类中创建静态方法等函数
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity()
export class User extends BaseEntity {  // extends BaseEntity
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column()
  isActive: boolean;

  // 创建静态方法函数
  static findByName(firstName: string, lastName: string) {
    return this.createQueryBuilder("user")
      .where("user.firstName = :firstName", { firstName })
      .andWhere("user.lastName = :lastName", { lastName })
      .getMany();
  }
}
// 示例如何保存AR实体
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.isActive = true;
// 直接使用UserEntity 的实例 user来save
await user.save();

// 删除
await user.remove();

// 查询实体
const users = await User.find({ skip: 2, take: 5 });
const newUsers = await User.find({ isActive: true });
const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" });

// 使用实体类中的static 静态方法
const timber = await User.findByName("Timber", "Saw");

Data Mapper (推荐)

使用 Data Mapper 方法,你可以在称为“存储库”(Repository)的单独类中定义所有查询方法,并使用存储库保存、删除和加载对象:

const userRepository = connection.getRepository(User);

// 示例如何保存DM实体
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.isActive = true;
// 存储库Repository实现数据操作
await userRepository.save(user);  

// 删除
await userRepository.remove(user);

// 查询实体
const users = await userRepository.find({ skip: 2, take: 5 });
const newUsers = await userRepository.find({ isActive: true });
const timber = await userRepository.findOne({ firstName: "Timber", lastName: "Saw" });

静态方法添加: 假设我们要创建一个按 first name 和 last name 返回用户的函数(类似于上面Active Record的static静态方法),可以在"custom repository"中创建这样的功能。

import { EntityRepository, Repository } from "typeorm";
import { User } from "../entity/User";

@EntityRepository()
export class UserRepository extends Repository<User> {
  findByName(firstName: string, lastName: string) {
    return this.createQueryBuilder("user")
      .where("user.firstName = :firstName", { firstName })
      .andWhere("user.lastName = :lastName", { lastName })
      .getMany();
  }
}

使用哪种方式?

在软件开发中我们应该始终牢记的一件事是我们如何维护它。

  • Data Mapper方法可以帮助你保持软件的可维护性,这在更大的应用程序中更有效。
  • Active record方法可以帮助你保持简单,这在小型应用程序中运行更好。 简单性始终是提高可维护性的关键。

使用 cascades 自动建立表关联关系

如上面步骤缩写

下面代码中userEntity与WorkItemEntity相互之间约定了@OneToMany与@ManyToOne实现相互关联,如果只是单向关联则可以不写其中一半(如只写了user - @OneToMany - workItem, 则只能通过UserEntity查询到userWorkItemInfo,从workItemEntity则无法查询到user信息了)。

注意: cascade 需要写在ManyToOne这一侧

// user.entity.ts
  @OneToMany(() => WorkItemEntity, (m) => m.userInfo)
  userWorkItemInfo: WorkItemEntity[];


// workItem.entity.ts
  @ManyToOne(() => UserEntity, (m) => m.userWorkItemInfo, {
    cascade: true,  // 注意cascade 需要写在ManyToOne这一侧
    onDelete: 'CASCADE',
    onUpdate: 'CASCADE',
  })
  @JoinColumn({ name: 'userId' })
  userInfo: UserEntity;

再举个栗子

photo 的@OneToOne装饰器:

export class Photo {
  /// ... other columns
  @OneToOne(type => PhotoMetadata, metadata => metadata.photo, {
    cascade: true
  })
  metadata: PhotoMetadata;
}

使用cascade允许就不需要手动存储 photo 和metdata两个实体。我们可以直接保存一个 photo 对象,由于使用了 cascade,关联的metadata 也将自动保存。

createConnection(options)
  .then(async connection => {
    // 创建 photo 对象
    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.isPublished = true;

    // 创建 photo metadata 对象
    let metadata = new PhotoMetadata();
    metadata.height = 640;
    metadata.width = 480;
    metadata.compressed = true;
    metadata.comment = "cybershoot";
    metadata.orientation = "portait";

    photo.metadata = metadata; // this way we connect them

    // 获取 repository
    let photoRepository = connection.getRepository(Photo);

    // 保存photo的同时保存metadata
    await photoRepository.save(photo);

    console.log("Photo is saved, photo metadata is saved too.");
  })
  .catch(error => console.log(error));

查询 新增 删除 编辑 方法

1、查询(find findOne…………)

// 这是****service.ts 文件
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '../entities/user.entity';
import { ProjectManageEntity } from '../entities/projectManage.entity';
import { WorkItemEntity } from '../entities/workItem.entity';
import { HalfMonthEntity } from '../entities/halfMonth.entity';
@Injectable()
export class ProjectManageService {
  constructor(
    @InjectRepository(UserEntity)
    private readonly UserRepository: Repository<any>,
  @InjectRepository(ProjectManageEntity)
  private readonly projectRepository: Repository<any>,
  @InjectRepository(WorkItemEntity)
  private readonly workItemRepository: Repository<any>,
  @InjectRepository(HalfMonthEntity)
  private readonly halfMonthRepository: Repository<any>,
  ) {}

async queryWay('这里存放是controller传递过来的参数') {
  //01
  let data =  await this.projectRepository
    .createQueryBuilder('projectInfo')//名字随便取
    .where('projectInfo.leadId = :leadId', {
      leadId: '传递过来的参数',
    })
    .getMany();

  //02 findOne
  const leader = this.UserRepository.findOne({ id: '传递过来的参数' });
  const user = await this.projectRepository.findOne('传递过来的参数');

  //03 find
  let where = {status:0}//条件
  const list = await this.projectRepository.find();
  const list = await this.projectRepository.find({
    relations: ['leader'],
    where,
    order: {createTime: 'ASC',},
  });

  const where = {
    halfMonthName: '传递过来的参数',
    projectInfo: { leader: { id: '传递过来的参数' } },
  };
  // 04 通过relations连表查询
  const list = await this.halfMonthRepository.find({
    relations: ['workItemInfo', 'projectInfo', 'projectInfo.leader', 'workItemInfo.userInfo'],
    where,
    order: {
      projectInfo: 'ASC',
    },
  });

  //05 QueryBuilder联表查询
  const list = await this.halfMonthRepository
    .createQueryBuilder('halfMonthQuery') //名字随便取      
    .leftJoinAndSelect('halfMonthQuery.workItemInfo', 'workItemInfo')
    .leftJoinAndSelect('halfMonthQuery.projectInfo', 'projectInfo')
    .where('halfMonthQuery.halfMonthName = :halfMonthName1 and workItemInfo.userId = :authId1', {
      halfMonthName1: '传递过来的参数',
      authId1:'传递过来的参数',
    })
    .getMany();
  }
}

2、新增(create save)

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '../entities/user.entity';
import { Repository } from 'typeorm';
export class UserService {
  constructor(
    @InjectRepository(UserEntity)
    private userRepository: Repository<UserEntity>,
  ) {}
  //创建
  async addUser(createUser) {
    // create 只是将数据转化为Entity的实例而已,如果有多余的字段,会在create之后返回的实例中去除
    //下面的create也可以省略 
    const newObj = await this.userRepository.create({
        ...createParam,
        leader,
        haha: '我是多余字段',
      });
    // newObj 中会去除haha字段
      const res = await this.userRepository.save({ ...createUser, leader });
  }
}

3、删除(delete)

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '../entities/user.entity';
import { Not, Repository } from 'typeorm';
export class UserService {
  constructor(
    @InjectRepository(UserEntity)
    private userRepository: Repository<UserEntity>,
  ) {}
  async delUser(id) {
    return this.userRepository.delete({
      id: id,
      role: Not('super'), // role != 'super'
    });
  }
}

4、编辑(update)

import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { ProjectManageEntity } from '../entities/projectManage.entity';
@Injectable()
export class ProjectManageService {
    constructor(
    private readonly UserRepository: Repository<any>,
    @InjectRepository(ProjectManageEntity)
  ) {}
  async addProject(createParam) {
    if ('id' in createParam) {
      const updateRes = await this.projectRepository.update(
        {
          id: createParam.id,
        },
        {
          projectName: createParam.projectName,
          projectNum: createParam.projectNum,
          projectMilestone: createParam.projectMilestone,
          devotionTime: createParam.devotionTime,
        },
      );
      return updateRes;
    }
  }
}

TypeOrm下树形结构(Tree)实体类创建与查询

项目中经常会有组织机构等树形结构的递归场景,数据接口如下:

[{    "id": 1,    "name": "a1",    "children": [{        "id": 2,        "name": "a11",        "children": [{            "id": 4,            "name": "a111"        }, {            "id": 5,            "name": "a112"        }]
    }, {
        "id": 3,
        "name": "a12"
    }]
}]

TypeORM 给我们提供了更加便捷的方式来进行存储和查询,官网链接: typeorm.bootcss.com/tree-entiti…

实体定义案例如下:

import {
  Column,
  Entity,
  PrimaryGeneratedColumn,
  OneToMany,
  Tree,
  TreeChildren,
  TreeParent,
} from 'typeorm';
import { UserEntity } from './user.entity';
import { ProjectManageEntity } from './projectManage.entity';

@Entity('tbl_department')
@Tree('closure-table') // 数据库存储方式,还可支持其他模式存储
export class DepartmentEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
    type: 'varchar',
    name: 'depart_name',
    length: 120,
    nullable: false,
    comment: '部门名称',
  })
  departName: string;

  @Column({
    type: 'varchar',
    name: 'short_name',
    length: 80,
    comment: '部门简称',
  })
  shortName: string;

  @TreeChildren()
  children: DepartmentEntity[];  // 定义children

  @TreeParent()
  parent: DepartmentEntity;  // 定义父级

  @OneToMany(() => UserEntity, (m) => m.department)
  userList: UserEntity[];

  @OneToMany(() => ProjectManageEntity, (m) => m.department)
  projectList: ProjectManageEntity[];
}

service.ts 中使用

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TreeRepository } from 'typeorm';
import { DepartmentEntity } from '../entities/department.entity';

@Injectable()
export class DepartmentService {
  constructor(
    @InjectRepository(DepartmentEntity)
    private readonly departmentRepository: TreeRepository<DepartmentEntity>,
  ) {}
  // 整个组织机构树
  async departmentTreeQuery() {
    return await this.departmentRepository.findTrees();
  }

  // 当前组织以及相关用户和项目
  async findDepartRelations(departId) {
    return await this.departmentRepository.findOne(
      { id: departId },
      {
        relations: ['userList', 'projectList'],
      },
    );
  }

  // 当前组织和后代组织
  async findDescendantsTree(department) {
    return await this.departmentRepository.findDescendantsTree(department);
  }

  // 当前组织和后代组织,包含关联用户和项目
  async findDescendantsTreeRelations() {
    return await this.departmentRepository.findTrees({
      relations: ['userList', 'projectList'],
    });
  }

  async addDepartment(params) {
    const { parentId } = params;
    let parent;
    if (parentId) {
      parent = await this.departmentRepository.findOne(parentId);
    }
    return await this.departmentRepository.save({ ...params, parent });
  }

  async editDepartment(id, depart) {
    return await this.departmentRepository.update({ id }, depart);
  }

  async delDepartment(id) {
    return await this.departmentRepository.delete({ id });
  }
}

几个常用api方法:

  • findTrees - 返回数据库中所有树,包括所有子项,子项的子项等。
  • findRoots - 根节点是没有祖先的实体。 找到所有根节点但不加载子节点。
  • findDescendants - 获取给定实体的所有子项(后代)。 将它们全部返回到数组中。

其他可查看官网链接。

手动事务管理

import { TreeRepository, getConnection, getManager } from 'typeorm';

// 手动事务管理与手写sql
  await getManager().transaction(async (transactionalEntityManager) => {
    // 内部只能使用回调中的transactionalEntityManager
    const depart = await transactionalEntityManager.insert(DepartmentEntity, params);
    const id = depart.raw[0].id;
    if (!id) {
      console.log('初始化组织机构失败', depart);
      throw new Error(`初始化组织机构失败`);
    }
    // 手写sql
    await transactionalEntityManager.query(
      `INSERT INTO "tbl_department_closure" ("id_ancestor", "id_descendant") VALUES (${id}, ${id})`,
    );
  });

使用注解实现事务(TODO)

手写sql语句实现查询

有的时候Typeorm提供的方法满足不了我们的查询需要,可以自己写sql语句实现,如下:

  queryProjectSumListByUser(halfMonth, userIdList) {
    const userIds = userIdList.join("','"); // 注意字符串被拼接进去之后需要有单引号
    const entityManager = getManager(); // 手写sql
    return entityManager.query(
      `select "projectId", SUM("workDays") as "projectSumDays" from "workItem" W where half_month = '${halfMonth}' and  "userId" IN ('${userIds}') GROUP BY "projectId"`,
    );
  }

HttpException异常情况

异常情况在main.ts中处理HttpException异常

import { HttpExceptionFilter } from './core/filter/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  //过滤处理 HTTP 异常
  app.useGlobalFilters(new HttpExceptionFilter());
  ...  ...
}

http-exception.filter.ts

import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp(); // 获取请求上下文
    const response = ctx.getResponse(); // 获取请求上下文中的 response对象
    const status = exception.getStatus(); // 获取异常状态码
    const exceptionResponse = exception.getResponse();
    // 设置错误信息
    const errorResponse = {
      data: {},
      msg: exceptionResponse?.message || exception.message,
      code: exceptionResponse?.statusCode || status,
    };

    // 设置返回的状态码, 请求头,发送错误信息
    response.status(status);
    response.header('Content-Type', 'application/json; charset=utf-8');
    response.send(errorResponse);
  }
}

多环境项目配置

需求:项目中数据库、运行端口、初始化数据等均需要在配置文件中约定,且需要在开发环境

测试环境和生产环境等多个环境实现切换

安装

npm i --save @nestjs/config

添加配置

在项目src文件夹下新增一个config文件夹,接着在在config文件夹下,新增 development.ts(开发环境) production.ts(线上环境)index.ts 这3个文件

//development.ts
export default {
  //端口
  port: parseInt(process.env.PORT, 10) || 3000,
  //数据库配置
  DATABASE_CONFIG: {
    type: 'postgres', // 数据库类型
    entities: ['dist/**/*.entity{.ts,.js}'], // 数据表实体
    host: '192.168.78.183', // 主机,默认为localhost
    port: 5433, // 端口号
    username: 'postgres', // 用户名
    password: '*********', // 密码
    database: 'semiMonthlyReportDev', //数据库名
    timezone: '+08:00', //服务器上配置的时区
    synchronize: false, //根据实体自动创建数据库表, 生产环境建议关闭
    logging: false,
  },
};
//production.ts
export default {
  //端口
  port: parseInt(process.env.PORT, 10) || 3000,
  //数据库配置
  DATABASE_CONFIG: {
    type: 'postgres', // 数据库类型
    entities: ['dist/**/*.entity{.ts,.js}'], // 数据表实体
    host: '192.168.78.183', // 主机,默认为localhost
    port: 5433, // 端口号
    username: 'postgres', // 用户名
    password: '*********', // 密码
    database: 'semiMonthlyReport', //数据库名
    timezone: '+08:00', //服务器上配置的时区
    synchronize: false, //根据实体自动创建数据库表, 生产环境建议关闭
    logging: false,
  },
};
//index.ts 
import developmentConfig from './development';
import productionConfig from './production';

const configs = {
  development: developmentConfig,
  production: productionConfig,
};

const env = process.env.NODE_ENV || 'development';
console.log('现在的环境是', env);
export default () => configs[env];

使用

//app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService, ConfigModule } from '@nestjs/config';
import { UserModule } from './user/user.module';
import { ProjectManageModule } from './project-manage/project-manage.module';
import customConfig from './config';
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true, // 设置为全局
      load: [customConfig], //加载自定义配置
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService], //注入服务
      useFactory: (configService: ConfigService) => configService.get('DATABASE_CONFIG'),
    }),
    UserModule,
    ProjectManageModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

JWT 实现登录

JWT 登录流程:

  1. 客户端用户进行登录请求;
  2. 服务端拿到请求,根据参数查询用户表;
  3. 若匹配到用户,将用户信息进行签证,并颁发 Token;
  4. 客户端拿到 Token 后,存储至某一地方,在之后的请求中都带上 Token ;
  5. 服务端接收到带 Token 的请求后,直接根据签证进行校验,无需再查询用户信息;

实现方式

下面我们从接口的流程从前到后说明

依赖

npm i passport passport-jwt @nestjs/passport @nestjs/jwt
(1)先从接口进入
@Post('login')
// 如果使用@Response()需要使用res.send返回接口信息,使用原来的return无法返回响应
async login(@Body() info: LoginDto, @Response() res) {
  try {
    const { account, pwd } = info;
    const person = await this.userService.findUser(account); 
    // 登录失败
    if (person.pwd !== pwd) {
      throw new Error('账号或密码错误');
    }
    //登录成功
// 签发token: 此处将用户id和所属的组织机构id(后面接口会根据组织机构id做数据筛选)作为参数生成token
    let tokenParams = {
      userId: person.id,
    };
    // 按需添加departId属性:有的用户不属于任何组织机构,则不加
    if (person.department) {
      tokenParams['departId'] = person.department.id;
    }
    const token = this.certificationService.genToken(tokenParams);
    // 如通过Cookie来做也可以
    // res.cookie('auth', token, {
    //   httpOnly: true, // 前端不可获取和修改cookie,只能在http请求中自行携带
    //   signed: true,
    // });
    res.json(
      ResultData.success({
        account: person.account,
        userName: person.userName,
        role: person.role,
        userProjectCount: person.userProjectInfo?.length,
        auth: token.accessToken,
      }),
    );
  } catch (err) {
    res.json(ResultData.fail(AppHttpCode.LOGIN_FAIL, err.message));
  }
}
(2)生成、校验、解析token
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class CertificationService {
  public constructor(
    @InjectRepository(UserEntity) private userRepository: Repository<UserEntity>,
    private readonly configService: ConfigService,
    private readonly jwtService: JwtService,
  ) {}
/**
   * 生成 token 与 刷新 token
   * @param payload
   * @returns
   */
  genToken(payload: { userId: string; departId?: number }) {
    const accessToken = `Bearer ${this.jwtService.sign(payload)}`;
    const refreshToken = this.jwtService.sign(payload, {
      expiresIn: this.configService.get('JWT.refreshExpiresIn'),
    });
    return { accessToken, refreshToken };
  }

 /**
   * 生成刷新 token
   */
  refreshToken(id: string): string {
    return this.jwtService.sign({ id });
  }

  /** 校验 token */
  async verifyToken(token: string) {
    try {
      if (!token) return null;
      const id = await this.jwtService.verify(token.replace('Bearer ', ''));
      return id;
    } catch (error) {
      return null;
    }
  }
}
(3)jwt初始化类配置

jwt.strategy.ts

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

/**
 * 这里的构造函数向父类传递了授权时必要的参数,在实例化时,父类会得知授权时,客户端的请求必须使用 Authorization 作为请求头,
 * 而这个请求头的内容前缀也必须为 Bearer,在解码授权令牌时,使用秘钥 secretOrKey: 'secretKey' 来将授权令牌解码为创建令牌时的 payload。
 */
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly config: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: config.get('JWT.secretkey'),
    });
  }

  /**
   * validate 方法实现了父类的抽象方法,在解密授权令牌成功后,即本次请求的授权令牌是没有过期的,
   * 此时会将解密后的 payload 作为参数传递给 validate 方法,这个方法需要做具体的授权逻辑,比如这里我使用了通过用户名查找用户是否存在。
   * 当用户不存在时,说明令牌有误,可能是被伪造了,此时需抛出 UnauthorizedException 未授权异常。
   * 当用户存在时,会将 user 对象添加到 req 中,在之后的 req 对象中,可以使用 req.user 获取当前登录用户。
   */
  async validate(payload: any) {
    console.log(`JWT验证 - Step 4: 被守卫调用`);
    const info = payload.info;
    //注意:为了防止jwt被编译出来,所以采用了加密方式,
    //注释掉的部份只是为了验证,可以不编写,如果有其他需求的话按该方式进行解码
    // const userInfo = crypto.AES.decrypt(info, 'salt').toString(crypto.enc.Utf8);
    // console.log(JSON.parse(userInfo));
    //return的信息会被放在request里的user部份,可以通过 ctx.switchToHttp().getRequest().user进行获取
    return {
      info,
    };
  }
}
(4) module注入

certification.module.ts

import { Module } from '@nestjs/common';
import { CertificationController } from './certification.controller';
import { CertificationService } from './certification.service';
import { JwtStrategy } from './jwt.strategy';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (config: ConfigService) => ({
        secret: config.get('JWT.secretkey'),
        signOptions: {
          expiresIn: config.get('JWT.expiresin'),
        },
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [CertificationController],
  providers: [CertificationService, JwtStrategy],
  exports: [CertificationService],
})
export class CertificationModule {}
(5) Guard守卫校验token

auth.guard.ts

import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import {
  ExecutionContext,
  ForbiddenException,
  Inject,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { CheckTokenFlag } from '../decorators/notCheckToken.decorator';
import { CertificationService } from '../certification/certification.service';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(
    private readonly reflector: Reflector,
    @Inject(CertificationService)
    private readonly certificationService: CertificationService,
  ) {
    super();
  }

  async canActivate(ctx: ExecutionContext): Promise<boolean> {
    const req = ctx.switchToHttp().getRequest();
    // const res = ctx.switchToHttp().getResponse()
    const accessToken = req.get('Auth');
    if (!accessToken) throw new ForbiddenException('请先登录');
    const { userId, departId } = await this.certificationService.verifyToken(accessToken);
    if (!userId) throw new UnauthorizedException('当前登录已过期,请重新登录');
    return true;
  }

  async activate(ctx: ExecutionContext): Promise<boolean> {
    return super.canActivate(ctx) as Promise<boolean>;
  }
}

绑定到 Module 上后,我们可以在 controller 上使用守卫装饰器 @UseGuards(AuthGuard) 验证是否生效, demo 如下

import { JwtAuthGuard } from '../guards/auth.guard';


//查询
  @Get('list')
  @UseGuards(JwtAuthGuard) // 守卫只会控制这个方法
  async findAll(@Headers() headers) {
    try {
      console.log('Header', headers.auth, headers.jsc);
      const list = await this.userService.findAll();
      return ResultData.success(list);
    } catch ({ message }) {
      return ResultData.fail(AppHttpCode.COMMON_ERR, message);
    }
  }

But 缺点

这种方式有一个缺点是需要在每个 controller 都需要写上 @UseGuards(AuthGuard)

下面我们使用app的全局守卫来修改一下

// app.module.ts

@Module({
  ...

   providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard
    }
  ]
}

有的接口不校验怎么办

当我们使用全局守卫时,所有接口请求头都必须带上 token ,而实际情况项目有的接口是不需要 token 的,如 登录、注册等,那我们必须提供一种机制来将接口路由不校验 token

编写自定义装饰器
// 自定义装饰器:提供一种机制来将某些有该注解的接口不校验 token
import { SetMetadata } from '@nestjs/common';

export const CheckTokenFlag = false;
/**
 * 允许 接口 不校验 token
 */
export const NotCheckToken = () => SetMetadata(CheckTokenFlag, true); // 将CheckTokenFlag 置为 true

修改auth.guard.ts

import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import {
  ExecutionContext,
  ForbiddenException,
  Inject,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { CheckTokenFlag } from '../decorators/notCheckToken.decorator';
import { CertificationService } from '../certification/certification.service';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(
    private readonly reflector: Reflector,
    @Inject(CertificationService)
    private readonly certificationService: CertificationService,
  ) {
    super();
  }

  async canActivate(ctx: ExecutionContext): Promise<boolean> {
    // !!!重点:::是否允许 无 token 访问;获取到具有@NotCheckToken() 注解的接口方法的值
    const pass = this.reflector.getAllAndOverride<boolean>(CheckTokenFlag, [
      ctx.getHandler(),
      ctx.getClass(),
    ]);
    if (pass) return true;

    const req = ctx.switchToHttp().getRequest();
    // const res = ctx.switchToHttp().getResponse()
    const accessToken = req.get('Auth');
    if (!accessToken) throw new ForbiddenException('请先登录');
    const { userId, departId } = await this.certificationService.verifyToken(accessToken);
    if (!userId) throw new UnauthorizedException('当前登录已过期,请重新登录');
    return true;
  }

  async activate(ctx: ExecutionContext): Promise<boolean> {
    return super.canActivate(ctx) as Promise<boolean>;
  }
}

装饰器使用案例

import { NotCheckToken } from '../decorators/notCheckToken.decorator';
  //查询
  @Get('queryList')
  @NotCheckToken()
  async findAll(@Query() query) {
    try {
      const weekdays = this.configService.get('WEEKDAYS');
      query['weekdays'] = weekdays;
      const data = await this.projectManageService.findAll(query);
      return ResultData.success(data);
    } catch (err) {
      return ResultData.fail(AppHttpCode.COMMON_ERR, err?.message);
    }
  }

总结:

到此我们就实现了jwt的登录与全局token守卫校验,同时也支持了部分接口不需要校验的自定义注解方式!!!简直完美

文件上传与下载

创建 album模块

nest g mo album   //创建 album 模块
nest g co album	 //创建控制器
nest g service album //创建服务类
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { ConfigService } from 'nestjs-config';
import { AlbumController } from './album.controller';
import { AlbumService } from './album.service';

@Module({
  imports: [
    MulterModule.registerAsync({
      useFactory: (config: ConfigService) => config.get('file'),
      inject: [ConfigService],
    }),
  ],
  controllers: [AlbumController],
  providers: [AlbumService],
})
export class AlbumModule {}
import {
  Controller,
  Post,
  UploadedFile,
  UseInterceptors,
  Get,
  Res,
} from '@nestjs/common';
import { Response } from 'express';
import { FileInterceptor } from '@nestjs/platform-express';
import { AlbumService } from './album.service';

@Controller('album')
export class AlbumController {
  constructor(private readonly albumService: AlbumService) {}

  @Post()
  @UseInterceptors(FileInterceptor('file'))
  upload(@UploadedFile() file) {
    this.albumService.upload(file);
    return true;
  }

  @Get('export')
  async downloadAll(@Res() res: Response) {
    const { filename, tarStream } = await this.albumService.downloadAll();
    res.setHeader('Content-Type', 'application/octet-stream');
    res.setHeader(
      'Content-Disposition',
      `attachment; filename=${filename}`,
    );
    tarStream.pipe(res);
  }
}
import { Injectable } from '@nestjs/common';
import { tar } from 'compressing';
import { ConfigService } from 'nestjs-config';

@Injectable()
export class AlbumService {
  constructor(private readonly configService: ConfigService) {}
  //上传
  upload(file) {
    console.log(file);
  }
  //下载
  async downloadAll() {
    const uploadDir = this.configService.get('file').root;
    const tarStream = new tar.Stream();
    await tarStream.addEntry(uploadDir);
    return { filename: 'hello-world.tar', tarStream };
  }
}

附录

依赖注入与控制反转的概念解释

依赖注入DI和控制反转IOC
•  IoC——Inversion of Control  控制反转
•  DI——Dependency Injection   依赖注入

要想理解上面两个概念,就必须搞清楚如下的问题:
• 参与者都有谁?
• 依赖:谁依赖于谁?为什么需要依赖?
• 注入:谁注入于谁?到底注入什么?
• 控制反转:谁控制谁?控制什么?为何叫反转(有反转就应该有正转了)?
• 依赖注入和控制反转是同一概念吗?
下面就来简要的回答一下上述问题,把这些问题搞明白了,IoC/DI也就明白了。

参与者都有谁

一般有三方参与者:一个是某个对象;一个是IoC/DI的容器;另一个是某个对象的外部资源。

  • 某个对象指的就是任意的、普通的Java对象;
  • IoC/DI的容器简单点说就是指用来实现IoC/DI功能的一个框架程序;
  • 对象的外部资源指的就是对象需要的,但是是从对象外部获取的,都统称资源,比如:对象需要的其它对象、或者是对象需要的文件资源等等。

谁依赖于谁,为什么需要依赖

当然是某个对象依赖于IoC/DI的容器

对象需要IoC/DI的容器来提供对象需要的外部资源

谁注入于谁,到底注入什么

很明显是IoC/DI的容器 注入 某个对象

就是注入某个对象所需要的外部资源

谁控制谁,控制什么

当然是IoC/DI的容器来控制对象了
主要是控制对象实例的创建

为何叫反转

反转是相对于正转而言的,那么什么算是正转的呢?

考虑一下常规情况下的应用程序,如果要在A里面使用C,你会怎么做呢?当然是直接去创建C的对象,也就是说,是在A类中主动去获取所需要的外部资源C,这种情况被称为正转的。
那么什么是反向呢?就是A类不再主动去获取C,而是被动等待,等待IoC/DI的容器获取一个C的实例,然后反向的注入到A类中。

用图例来说明一下,先看没有IoC/DI的时候,常规的A类使用C类的示意图,如图所示:

当有了IoC/DI的容器后,A类不再主动去创建C了,如图所示:

而是被动等待,等待IoC/DI的容器获取一个C的实例,然后反向的注入到A类中,如图所示:

依赖注入和控制反转是同一概念吗?

\

根据上面的讲述,应该能看出来,依赖注入和控制反转是对同一件事情的不同描述,从某个方面讲,就是它们描述的角度不同。依赖注入是从应用程序的角度在描述,应用程序依赖容器创建并注入它所需要的外部资源;而控制反转是从容器的角度在描述,容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。

\

小结一下


其实IoC/DI对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC/DI容器来创建并注入它所需要的资源了。
这么小小的一个改变其实是编程思想的一个大进步,这样就有效的分离了对象和它所需要的外部资源,使得它们松散耦合,有利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

\

\

dto常用注解

// 验证非空字符串
@IsString()
@IsNotEmpty()
public name: string;

// 仅在它是请求正文的一部分时才进行验证
@IsString()
@IsNotEmpty()
@IsOptional()
public email: string;

// 验证整数
@IsNumber()
public age: number;

// 验证整数
@IsBoolean()
public acceptedTOS: boolean;

// 验证非空整数数组
@IsArray()
@IsNumber({ allowNaN: false }, { each: true })
@ArrayMinSize(1)
public nums: number[];

优秀开源项目

  1. wenqiyun.github.io/nest-admin/…

参考文章

干货!一篇能带你搞懂前端Nest.js核心原理的文章 (juejin.cn/post/711864…

依赖注入和控制反转(www.iteye.com/blog/baitai…