Nest实战 - 员工模块

lxf2023-05-05 18:28:02

前言

本系列主要通过实现一个后台管理系统作为切入点,帮助掘友熟悉nest的开发套路

本系列作为前后端分离项目,主要讲解nest相关知识,前端项目知识一笔带过

完整的示例代码在最后

本章属于实战系列第二章,主要讲解员工模块相关内容,包括

  • 头像上传本地文件上传阿里oss文件上传

  • 头像识别百度AI识别图像,部分数据完成自动填充

  • typeORM 公共字段填充@BeforeInsert@BeforeUpdate的使用和注意事项

  • 扩展 process.env 的类型

  • 数据库相关知识In Like 分页

  • Restful风格的代码开发

  • 密码md5 处理

技术栈

  • node:19.x
  • 后端:nest + mysql
  • 前端:react + redux + antd

规则

  • 本系列完全遵循RestFul风格进行开发,即Get Post Put Delete
  • 本系列的调用链 controller --> service --> 三方服务数据库
  • 本系列的三方服务被消费时,统一注入到nestIOC中,统一风格;其他函数的使用直接调用即可

员工模块-分页

介绍

  • 在本人目前的开发中,分页功能是最常见的功能
  • 好处
    • 服务端: 提升性能,减小内存的压力,查询效率高
    • 客户端: 页面渲染快,不然几万个DOM同时渲染,更改,页面直接GG

页面预览

  • Nest实战 - 员工模块
  • Nest实战 - 员工模块
  • Nest实战 - 员工模块
  • Nest实战 - 员工模块
  • Nest实战 - 员工模块

开发- controller

代码

  • 打开 EmployeeController,加入以下代码
      @ApiOperation({
        summary: '分页',
      })
      @Get('page')
      page(
        @Query('page') page: number,
        @Query('pageSize') pageSize: number,
        @Query('name') name?: string,
      ) {
        return this.employeeService.page(page, pageSize, name);
      }
    

说明

  • 正常来说,nest接收到的所有基本类型的参数类型都是string,上一章我们在AppModule中加入了全局管道验证,并设置了属性转换功能transformtrue,这样nest内部就可以通过ts的类型自动转换了 Nest实战 - 员工模块 喏,就是这块做的自动转换
  • @Query装饰器可以拿到url问号(?)后边的参数值
  • page当前页数,最少传1
  • pageSize 每页多少条
  • name 用户名,做模糊查询用
  • 接下来就把接收到的参数传入EmployeeServicepage方法

开发- service

代码

  • 打开EmployeeService,加入分页代码
      /**
       *
       * @param page 页数
       * @param pageSize 每页多少条
       * @param name 用户名
       * @returns 分页
       */
      async page(page: number, pageSize: number, name = '') {
        const [employeeList, total] = await this.employeeRepository.findAndCount({
          where: {
            name: Like(`%${name}%`),
          },
          skip: (page - 1) * pageSize,
          take: pageSize,
        });
    
        return new BasePage(page, pageSize, total, employeeList);
      }
    

说明

  • 上一章中注入了employeeRepository,所以这里可以直接调用 Nest实战 - 员工模块
  • employeeRepository.findAndCount方法返回一个promise数组,数组的第0项是查询到的对象数组,第1项是符合当前条件的总条数
  • name字段默认字符串 '',不然会被当作undefind处理
  • where 中的 Like 等同于 sql 语句中的 Like,Like(%${name}%)表示值左右模糊查询
  • skip表示跳过多少条数据
  • take 表示查询多少条数据,即(每页多少条)
  • skiptake对应sql中的LIMIT关键字
  • 执行查询操作时,sql语句如下所示 Nest实战 - 员工模块
  • 共执行了俩次sql语句第一句查询列表信息,第二句查询总条数
  • 注意在第一条sql语句中,有排序查询 Nest实战 - 员工模块
  • orderBy关键字,是通过添加到实体类Employee文件Entity装饰器中实现的,这样只要执行select语句的时候,在没有手动添加updateTime排序规则的时候,就总会按照updateTime的倒序进行排序 Nest实战 - 员工模块
  • page方法中最后返回了类BasePageBasePage只做了一件事情,就是对分页数据进行封装

开发工具类 - 封装分页数据

代码

  • 新建 src/common/database/pageInfo.ts,写入
    /**
     * 分页数据封装
     */
    export class BasePage<T> {
      constructor(
        private page: number,
        private pageSize: number,
        private total: number,
        private records: T[],
      ) {}
    }
    
    

说明

  • 分页数据每个模块都会使用,故进行封装,统一调用即可

前端开发 - 拦截器处理

  • Nest实战 - 员工模块

说明

  • 我们在登陆接口进行了jwt验证,故没有添加@isPublic()装饰器的接口都需要进行token验证
  • 我们会在登陆页面点击登陆的时候,把员工数据写入reduxNest实战 - 员工模块
  • 前端主要注意拦截器处理,其他的正常开发即可

前端分页 - 效果截图

  • 全量搜索 Nest实战 - 员工模块
  • 模糊搜索 Nest实战 - 员工模块

公共模块 - 文件上传/预览

介绍

  • 文件上传下载功能比较通用,且为了遵循软件单一原则,所以把它抽离成基础公共模块
  • 如果所示,我们会在新增 修改时使用文件上传 预览功能 Nest实战 - 员工模块

本地文件上传

安装

  • 安装@types/multer提供ts类型支持
    yarn add @types/multer
    

代码

  • 终端中执行

    nest g module base
    nest g service base
    nest g controller base
    
  • 会生成以下结构文件,当然手动创建也可以,注意⚠️手动创建的模块需要手动引入AppModule

    Nest实战 - 员工模块

  • 打开src/base/base.module.ts,写入

    import { Module } from '@nestjs/common';
    import { MulterModule } from '@nestjs/platform-express';
    import { BaseController } from './base.controller';
    import { BaseService } from './base.service';
    import { diskStorage } from 'multer';
    import { checkDirAndCreate } from 'src/common/utils';
    import { webcrypto } from 'crypto';
    @Module({
      imports: [
        MulterModule.register({
          storage: diskStorage({
            destination(req, file, callback) {
              const filePath = `public/uploads/${file.mimetype.split('/')[0]}/`;
              checkDirAndCreate(filePath);
              return callback(null, `./${filePath}`);
            },
            filename(req, file, callback) {
              console.log(req.file);
              const suffix = file.originalname.substring(
                file.originalname.lastIndexOf('.'),
              );
              const fileName = Date.now() + '-' + webcrypto.randomUUID() + suffix;
              callback(null, fileName);
            },
          }),
          fileFilter(req, file, callback) {
            return callback(null, true);
          },
        }),
      ],
      controllers: [BaseController],
      providers: [BaseService],
    })
    export class BaseModule {}
    
    
  • 新建src/common/utils/index.ts,写入

    import { existsSync, mkdirSync } from 'fs';
    /**
     * 创建文件夹
     * @param filePath 文件路径
     */
    export const checkDirAndCreate = (filePath: string) => {
      const pathArr = filePath.split('/');
      let checkPath = '.';
      for (let i = 0; i < pathArr.length; i++) {
        checkPath += `/${pathArr[i]}`;
        if (!existsSync(checkPath)) {
          mkdirSync(checkPath);
        }
      }
    };
    
    /**
     *
     * @param src 文件地址
     * @returns 获取文件后缀名
     */
    export const getFileSuffix = (src: string) => {
      return src.substring(src.lastIndexOf('.'));
    };
    
    /**
     * 类赋值-合并
     * @param oldVal 旧值
     * @param newVal 新值
     */
    export function classAssign<T extends object>(oldVal: T, newVal: T): T {
      for (const k in newVal) {
        oldVal[k] = newVal[k];
      }
    
      return oldVal;
    }
    
    
    
  • 新建src/base/base.controller.ts,写入

    import {
      Controller,
      Headers,
      Post,
      UploadedFile,
      UseInterceptors,
    } from '@nestjs/common';
    import { FileInterceptor } from '@nestjs/platform-express';
    import { ApiOperation, ApiTags } from '@nestjs/swagger';
    import { isPublic } from 'src/auth/constants';
    
    @ApiTags('公共模块')
    @Controller('base')
    export class BaseController {
      @ApiOperation({
        summary: '上传本地',
      })
      @isPublic()
      @Post('/uploadLocal')
      @UseInterceptors(FileInterceptor('file'))
      uploadLocal(
        @UploadedFile() file: Express.Multer.File,
        @Headers('host') host: string,
      ) {
        // 如果是 localhost 就加上http://
        if (!host.includes('://')) {
          host = `http://${host}`;
        }
        return `${host}/${file.path}`;
      }
    }
    
    

说明

  • 文件上传请求方式一定为post
  • 前端需要在headers中设置"Content-Type": "multipart/form-data"来传输二进制文件
  • 发起urlv1/base/uploadLocal请求方式为post的时候,首先会经过@isPublic()装饰器去放行(免认证),然后进入拦截器,传入FileInterceptor('file'),这时就会进入MulterModule.register方法中,也就是这块 Nest实战 - 员工模块
  • 接着会把文件写到public文件夹下 Nest实战 - 员工模块
  • 最后会走到BaseController中的uploadLocal方法中,接着我们通过装饰器@Headers('host')拿到headers中的host,对file.path做拼接后即可返回
  • 接着,我们用postman测试,发现数据已经被成功返回 Nest实战 - 员工模块

开启静态文件预览

  • 问题
    • 拿到后端返回的图片地址发现无法预览,会被nest的拦截器所拦截 Nest实战 - 员工模块
  • 打开 src/main.ts,写入
    // 开启静态文件预览
      app.useStaticAssets('public', {
        prefix: '/public/',
      });
    
  • 保存文件,刷新浏览器,图片出来了,完美,收工 Nest实战 - 员工模块

阿里oss文件上传

介绍

  • 阿里云对象存储OSS(Object Storage Service)是一款海量、安全、低成本、高可靠的云存储服务,提供99.9999999999%(12个9)的数据持久性,99.995%的数据可用性。多种存储类型供选择,全面优化存储成本。

前置准备 - 购买对象存储OSS

  • 第一步,打开官网,登录阿里云,账号和扫码都可以 Nest实战 - 员工模块
  • 第二步 选择对象存储OSS Nest实战 - 员工模块
  • 第三步,没有购买过OSS的,选择立即购买 Nest实战 - 员工模块
  • 第四步,买完之后选择管理控制台 Nest实战 - 员工模块
  • 第五步,刚开始进入,Bucket列表为空,创建Bucket即可 Nest实战 - 员工模块
  • 第六步,填入Bucket名称,选择资源组,读写权限设置公共读写,点击确定即可

Nest实战 - 员工模块

  • 第七步,确定后会跳转到这个页面,点击返回即可回到列表页面 Nest实战 - 员工模块
  • 第八步,打开帮助文档 Nest实战 - 员工模块
  • 第九步,找到Nodejs版本文件上传文档就可以愉快的阅读了 Nest实战 - 员工模块
  • 第十步,OSS构造函数需要的四个参数region accessKeyId accessKeySecret bucket,在这里可以找到
  • 回到列表页,点Bucket名称 Nest实战 - 员工模块
  • Bucket名称就是你的bucket
  • oss-cn-hangzhou就是你的region Nest实战 - 员工模块
  • 第十一步,点击accessKey管理 Nest实战 - 员工模块
  • 第十二步,选哪个都行,区别就是子用户权限更小些 Nest实战 - 员工模块
  • 第十三步,点击查看sercet,发送手机验证码,即可获取到accessKeyId accessKeySecret

Nest实战 - 员工模块

安装

  • 安装ali-oss
    yarn add ali-oss
    

代码

  • 打开根目录/.config/.dev.yml,将配置信息写入配置文件
    # 阿里
    ALI:
      accessKeyId: accessKeyId
      accessKeySecret: accessKeySecret
      oss:
        region: oss-cn-hangzhou
        bucket: nest-study-backend
    
    

Nest实战 - 员工模块

  • 新建src/common/ALI/oss.module.tssrc/common/ALI/oss.service.ts
  • 分别写入
    import { Module } from '@nestjs/common';
    import { AliOssService } from './oss.service';
    
    @Module({
      providers: [AliOssService],
      exports: [AliOssService],
    })
    export class AliOssModule {}
    
    
    /* eslint-disable @typescript-eslint/no-var-requires */
    import { Injectable } from '@nestjs/common';
    import { getConfig } from '../utils/ymlConfig';
    import { CustomException } from 'src/common/exceptions/custom.exception';
    
    import { webcrypto } from 'crypto';
    import * as path from 'path';
    import type { PutObjectResult } from 'ali-oss';
    const OSS = require('ali-oss');
    const moment = require('moment');
    
    /**
     * 阿里  oss  服务
     */
    @Injectable()
    export class AliOssService {
      // 通过静态方法获取app实例
      static getOssClient() {
        const { accessKeyId, accessKeySecret, oss } = getConfig('ALI');
        return new OSS({
          ...oss,
          accessKeyId,
          accessKeySecret,
        });
      }
    
      //   获取oss路径
      static getOssPath(suffix: string) {
        const ymd: string = moment().format('YYYY/MM/DD');
        //   格式  2023/01/17/uuid
        return `${ymd}/${webcrypto.randomUUID()}${suffix}`;
      }
    
      /**
       *
       * @param url
       * @param suffix
       * @returns 上传buffer 到 oss
       */
      async putLocal(url: string, suffix: string) {
        try {
          return AliOssService.getOssClient().put(
            AliOssService.getOssPath(suffix),
            path.normalize(url),
          );
        } catch (error) {
          throw new CustomException(error);
        }
      }
    
      /**
       *
       * @param buffer
       * @param suffix
       * @returns 上传buffer 到 oss
       */
      async putBuffer(buffer: Buffer, suffix: string): Promise<PutObjectResult> {
        try {
          return await AliOssService.getOssClient().put(
            AliOssService.getOssPath(suffix),
            buffer,
          );
        } catch (error) {
          throw new CustomException(error);
        }
      }
    
      /**
       * 删除资源
       * @param path 资源文件路径
       */
      async deleteFile(path: string) {
        try {
          await AliOssService.getOssClient().delete(path);
        } catch (error) {
          throw new CustomException(error);
        }
      }
    }
    
  • 修改src/base/base.module.ts,走oss上传,要注掉storage,不然会走本地存储,同时file.buffer也拿不到
    import { Module } from '@nestjs/common';
    import { MulterModule } from '@nestjs/platform-express';
    import { BaseController } from './base.controller';
    import { BaseService } from './base.service';
    import { diskStorage } from 'multer';
    import { checkDirAndCreate } from 'src/common/utils';
    import { webcrypto } from 'crypto';
    import { AliOssModule } from 'src/common/ALI/oss.module';
    import { BaiduFaceModule } from 'src/common/BAIDU/face.module';
    @Module({
      imports: [
        AliOssModule,
        BaiduFaceModule,
        MulterModule.register({
          // storage: diskStorage({
          //   destination(req, file, callback) {
          //     const filePath = `public/uploads/${file.mimetype.split('/')[0]}/`;
          //     checkDirAndCreate(filePath);
          //     return callback(null, `./${filePath}`);
          //   },
          //   filename(req, file, callback) {
          //     console.log(req.file);
          //     const suffix = file.originalname.substring(
          //       file.originalname.lastIndexOf('.'),
          //     );
          //     const fileName = Date.now() + '-' + webcrypto.randomUUID() + suffix;
          //     callback(null, fileName);
          //   },
          // }),
          fileFilter(req, file, callback) {
            return callback(null, true);
          },
        }),
      ],
      controllers: [BaseController],
      providers: [BaseService],
    })
    export class BaseModule {}
    
    
  • 修改 src/base/base.controller.ts增加uploadOSS接口
    import {
      Controller,
      Headers,
      Post,
      UploadedFile,
      UseInterceptors,
    } from '@nestjs/common';
    import { FileInterceptor } from '@nestjs/platform-express';
    import { ApiOperation, ApiTags } from '@nestjs/swagger';
    import { isPublic } from 'src/auth/constants';
    import { getFileSuffix } from '../common/utils/index';
    import { AliOssService } from '../common/ALI/oss.service';
    
    @ApiTags('公共模块')
    @Controller('base')
    export class BaseController {
      constructor(private readonly aliOssService: AliOssService) {}
      @ApiOperation({
        summary: '上传本地',
      })
      @isPublic()
      @Post('/uploadLocal')
      @UseInterceptors(FileInterceptor('file'))
      uploadLocal(
        @UploadedFile() file: Express.Multer.File,
        @Headers('host') host: string,
      ) {
        console.log(host, file);
    
        // 如果是 localhost 就加上http://
        if (!host.includes('://')) {
          host = `http://${host}`;
        }
        return `${host}/${file.path}`;
      }
    
      @ApiOperation({
        summary: '上传阿里OSS',
      })
      @isPublic()
      @Post('/uploadOSS')
      @UseInterceptors(FileInterceptor('file'))
      async uploadOSS(@UploadedFile() file: Express.Multer.File) {
        // oss文件上传
        const { url } = await this.aliOssService.putBuffer(
          file.buffer,
          getFileSuffix(file.originalname),
        );
    
        return url;
      }
    }
    
    

测试

  • 老规矩,打开postman,输入localhost:3000/v1/base/uploadOSS,开始测试 Nest实战 - 员工模块
  • 进入阿里云后台,点击Bucket,发现文件上传成功了 Nest实战 - 员工模块

小结

  • 俩种方式都可以实现文件的上传下载
  • 具体情况看公司喜好,个人建议放到OSS,安全而且按量收费也挺合适的
  • 本次以阿里云为例做了演示,其他云产品自行参考,套路都一样

公共模块 - 百度人脸识别

目的

  • 为了体现产品的智能化(少的操作做多的事情
  • 实现上传头像,自动填充生日和性别 Nest实战 - 员工模块

介绍

  • 快速检测人脸并返回人脸框位置,输出人脸150个关键点坐标,准确识别多种属性信息

前置准备 - 购买人脸检测产品

  • 第一步,打开官网,登陆百度AI,扫码和账号登录都可以 Nest实战 - 员工模块
  • 第二步,选择人脸检测与属性分析 Nest实战 - 员工模块
  • 第三步,点击立即使用 Nest实战 - 员工模块
  • 第三步,开通人脸识别 Nest实战 - 员工模块
  • 第四步,根据需求,开通对应的功能即可,注意,开通服务前,需要先进行充值 Nest实战 - 员工模块
  • 第五步,订单没有疑问,直接下一步 Nest实战 - 员工模块
  • 第六步,开通成功后,返回管理控制台即可 Nest实战 - 员工模块
  • 发现人脸检测服务成功启用 Nest实战 - 员工模块
  • 第七步,查看api文档 Nest实战 - 员工模块
  • 第八步,找到NodeSDK,就可以愉快的阅读了 Nest实战 - 员工模块
  • 第九步,APP_ID APP_KEY SECRET_KEY 可以在这里找到
  • 账户ID就是APP_ID Nest实战 - 员工模块
  • 安全认证中可以拿到APP_KEYSECRET_KEY Nest实战 - 员工模块

安装

  • 安装baidu-aip-sdk
    yarn add baidu-aip-sdk
    

代码

  • 打开根目录/.config/.dev.yml,将配置信息写入配置文件
    #百度
    BAIDU:
      appId: appId
      accessKey: accessKey
      secretKey: secretKey
    

Nest实战 - 员工模块

  • 新建src/common/BAIDU/face.module.tssrc/common/BAIDU/face.service.ts

  • 分别写入

    import { Module } from '@nestjs/common';
    import { BaiduFaceService } from './face.service';
    
    @Module({
      providers: [BaiduFaceService],
      exports: [BaiduFaceService],
    })
    export class BaiduFaceModule {}
    
    /* eslint-disable @typescript-eslint/no-var-requires */
    import { Injectable } from '@nestjs/common';
    import { CustomException } from '../exceptions/custom.exception';
    import { getConfig } from '../utils/ymlConfig';
    const AipFaceClient = require('baidu-aip-sdk').face;
    
    export interface FaceInfo {
      error_code: number;
      error_msg: string;
      log_id: number;
      timestamp: number;
      cached: number;
      result: {
        face_num: number;
        face_list: {
          face_token: string;
          location: {
            left: number;
            top: number;
            width: number;
            height: number;
            rotation: number;
          };
          face_probability: number;
          angle: {
            yaw: number;
            pitch: number;
            roll: number;
          };
          age: number;
          gender: {
            type: 'male' | 'female';
            probability: number;
          };
        }[];
      };
    }
    
    /**
     * 百度人脸识别
     */
    @Injectable()
    export class BaiduFaceService {
      // 新建一个对象,建议只保存一个对象调用服务接口
      static getFaceClient() {
        const { appId, accessKey, secretKey } = getConfig('BAIDU');
    
        return new AipFaceClient(appId, accessKey, secretKey);
      }
    
      async getFaceInfo(
        imageUrl: string,
        imageType = 'URL',
        options = {
          face_field: 'age,gender',
        },
      ) {
        try {
          const faceInfo: FaceInfo = await BaiduFaceService.getFaceClient().detect(
            imageUrl,
            imageType,
            options,
          );
          return faceInfo;
        } catch (error) {
          throw new CustomException(error);
        }
      }
    }
    
  • 修改src/base/base.controller.tsuploadOSS方法,添加调用人脸识别代码

      @ApiOperation({
        summary: '上传阿里OSS',
      })
      @isPublic()
      @Post('/uploadOSS')
      @UseInterceptors(FileInterceptor('file'))
      async uploadOSS(@UploadedFile() file: Express.Multer.File) {
        // oss文件上传
        const { url, name } = await this.aliOssService.putBuffer(
          file.buffer,
          getFileSuffix(file.originalname),
        );
    
        // 执行人脸识别函数
        const faceInfo = await this.baiduFaceService.getFaceInfo(url);
        // 如果人脸识别失败,删除阿里云存储的图片
        if (faceInfo.error_code !== 0) {
          await this.aliOssService.deleteFile(name);
          // 返回人脸识别错误提示
          throw new CustomException(faceInfo.error_msg);
        }
        // 返回阿里oss图片地址和人脸识别信息
        return { url, ...faceInfo };
      }
    

说明

  • BaiduFaceService中调用人脸识别的时候,options参数的face_field参数一定要显式的传入,否则不会返回相关属性 Nest实战 - 员工模块
  • 人脸识别失败的图片,直接删除即可,无效的文件会占用资源,毕竟阿里OSS是按量收费的 Nest实战 - 员工模块

测试

  • 老规矩,打开postman,输入localhost:3000/v1/base/uploadOSS,开始测试,数据成功返回,歪瑞古德 Nest实战 - 员工模块

员工模块 - 新增

页面预览

Nest实战 - 员工模块 Nest实战 - 员工模块

开发 - controller

代码

  • 打开EmployeeController,加入以下代码
      @ApiOperation({
        summary: '创建员工',
      })
      @Post()
      create(@Body() employee: Employee) {
        employee.password = md5('123456');
        return this.employeeService.create(employee);
      }
    

说明

  • @Body装饰器可以获取到 post请求 body中的数据
  • 创建初始密码,并对其进行md5加密
  • employee传入service层,进行数据库交互

开发 - service

代码

  • 打开EmployeeService,加入以下代码
      /**
       *
       * @param employee Employee
       * @returns 创建员工
       */
      create(employee: Employee) {
        return this.employeeRepository.save(classAssign(new Employee(), employee));
      }
    
    
    
  • 打开src/types/index.d.ts,加入对process.dev的扩展代码
    import { Request } from 'express';
    import { Employee } from '../employee/entities/employee.entity';
    
    export type TIdAndUsername = 'id' | 'username';
    
    declare module 'express' {
      interface Request {
        user: Pick<Employee, TIdAndUsername>;
      }
    }
    
    declare global {
      namespace NodeJS {
        interface ProcessEnv {
          RUNNING: string;
          id: Employee['id'];
        }
      }
    }
    
  • 打开src/auth/strategy/jwt.strategy.ts,将employee.id添加到process.env
    import { Injectable } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { ExtractJwt, Strategy } from 'passport-jwt';
    import { getConfig } from '../../common/utils/ymlConfig';
    import { Employee } from '../../employee/entities/employee.entity';
    import { TIdAndUsername } from '../../types/index';
    
    @Injectable()
    export class JwtStrategy extends PassportStrategy(Strategy) {
      constructor() {
        super({
          // 提供从请求中提取 JWT 的方法。我们将使用在 API 请求的授权头中提供token的标准方法
          jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
          //   false 将JWT没有过期的责任委托给Passport模块
          ignoreExpiration: false,
          //   密钥
          secretOrKey: getConfig('JWT')['secret'],
        });
      }
    
      //  jwt验证
      async validate(
        payload: Pick<Employee, TIdAndUsername> & { iat: number; exp: number },
      ) {
        if (!process.env.id) {
          process.env.id = payload.id;
        }
        return {
          id: payload.id,
          username: payload.username,
        };
      }
    }
    
    

Nest实战 - 员工模块

说明

  • 将传入的数据经过classAssign包装后,存入到数据库
  • classAssign函数对值进行和合并处理 Nest实战 - 员工模块
  • 由于在BaseEntity中添加了@BeforeInsert@BeforeUpdate俩个装饰器,可以在执行insertupdate之前做前置操作 Nest实战 - 员工模块
  • 这俩个装饰器中,统一处理了createTime updateTime createUser updateUser
  • 如果调用employeeRepository.save的时候直接传入employee参数,这俩个前置装饰器时不会生效的,应为 employee中没有insertupdate方法
  • sql语句如下 Nest实战 - 员工模块
  • 状态status设计数据库的时候,默认填充0,即启用状态

员工模块 - 根据id查询

开发 - controller

代码

  • 打开EmployeeController,加入以下代码
      @ApiOperation({
        summary: '根据ID查询',
      })
      @Get('/:id')
      findOne(@Param('id') id: string) {
        return this.employeeService.findById(id);
      }
    

说明

  • @Param装饰器可以拿到路径上的参数/employee/1,即1,然后封装到id属性中
  • id传入service层,调用数据库即可

开发 - service

代码

  • 打开EmployeeService,加入以下代码
      /**
       *
       * @param id id
       * @returns 根据ID查询
       */
      async findById(id: string) {
        const employee = await this.employeeRepository.findOneBy({ id });
        if (!employee) {
          throw new CustomException('id不存在');
        }
        return employee;
      }
    

说明

  • 根据id查询数据库,如果没查到数据直接抛出自定义异常信息
  • 有查询数据,直接返回,前端做数据回显

员工模块 - 更新员工

开发 - controller

代码

  • 打开EmployeeController,加入以下代码
      @ApiOperation({
         summary: '更新',
       })
       @Put()
       update(@Body() employee: Employee) {
         return this.employeeService.update(employee);
       }
    

说明

  • 将前端传入的参数直接传入service层即可

开发 - service

代码

  • 打开EmployeeService,加入以下代码
       /**
       *
       * @param employee
       * @returns 更新
       */
      async update(employee: Employee) {
        return !!(
          await this.employeeRepository.update(
            { id: employee.id },
            classAssign(new Employee(), employee),
          )
        ).affected;
      }
    

说明

  • 没有黑魔法,将数据存入数据库即可,然后返回状态 true 更新成功, false更新失败

员工模块 - 删除员工

开发 - controller

代码

  • 打开EmployeeController,加入以下代码
     @ApiOperation({
         summary: '删除,支持批量操作',
       })
       @Delete()
       del(@Query('ids') ids: string[]) {
         return this.employeeService.delete(ids);
       }
    

说明

  • 前端会传入字符串/ids=1,2,3这样的格式,由于我们前面添加了全局管道转换,nest会根据ts类型,进行自动转换
  • 将转换后的数据传入service层即可

开发 - service

代码

  • 打开EmployeeService,加入以下代码
         /**
       *
       * @param ids ids
       * @returns 删除
       */
      async delete(ids: string[]) {
        // 只能删除停用的账号
        const count = await this.employeeRepository.countBy({
          id: In(ids),
          status: 1,
        });
        if (count > 0) {
          throw new CustomException('不能删除启用中的账号');
        }
        return !!(await this.employeeRepository.delete({ id: In(ids) })).affected;
      }
    

说明

  • 启用中的账号是不能删除的,可以通过count进行查询
  • In等同sql中的 IN 关键字

员工模块 - 设置启用 - 禁用

开发 - controller

代码

  • 打开EmployeeController,加入以下代码
    @ApiOperation({
     summary: '启用,禁用,支持批量操作',
    })
    @Post('status/:status')
    setStatus(@Param('status') status: number, @Query('ids') ids: string[]) {
     return this.employeeService.setStatus(ids, status);
    }
    

说明

  • 将接收到的statusids直接传入service层即可

开发 - service

代码

  • 打开EmployeeService,加入以下代码
       /**
       *
       * @param ids ids
       * @returns 设置员工状态  启用 - 禁用
       */
      async setStatus(ids: string[], status: number) {
        const employee = new Employee();
        employee.status = status;
        return !!(await this.employeeRepository.update({ id: In(ids) }, employee))
          .affected;
      }
    

说明

  • 没有黑魔法,直接根据id更改status即可

员工模块 - 导出全量数据

开发

  • 安装依赖xlsx

     yarn add xlsx
    
  • 打开EmployeeController,加入以下代码

      @ApiOperation({
         summary: '导出',
       })
       @Get('export')
       async exportXlsx(@Res() res: Response) {
         const allData = await this.employeeService.findAll();
         const buf = exportExcel(allData, '员工信息.xlsx');
         res.set(
           'Content-Disposition',
           'attachment; filename=' + encodeURIComponent('员工信息.xlsx') + '',
         );
         res.send(buf);
       }
    
  • 打开EmployeeService,加入以下代码

     /**
       *
       * @returns 查询所有数据
       */
      findAll() {
        return this.employeeRepository.find();
      }
    
  • 新建 src/common/utils/fileExport.ts,写入 ``` import * as XLSX from 'xlsx';

      /**
       *
       * @param data 数据
       * @param sheetName 工作簿名称
       * @returns 导出excel
       */
      export const exportExcel = <T>(data: T[], sheetName: string) => {
        const ws = XLSX.utils.json_to_sheet(data);
        const wb = XLSX.utils.book_new();
        XLSX.utils.book_append_sheet(wb, ws, sheetName);
        return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
      };
    
      ```
      
    

效果

Nest实战 - 员工模块 Nest实战 - 员工模块

说明

  • 前端调用/v1/employee/export,导出用户全量数据
  • 后端生成xlsx表格,通过二进制流的方式返回前端,前端拿到流数据,通过a标签的方式下载即可,前后端仓库代码,统一放在本章最后了

总结

  • 到现在nest部分的员工模块已经全部开发完成
  • 前置工作比较复杂,需要考虑代码的健壮性,以及阿里云百度AI的账号产品服务开通
  • 代码封装完成后,CRUD其实就很简单了,
  • 最后就是需要多理解业务需求,根据业务去拆分、整合代码,尽量遵循开闭原则单一原则

写在最后

  • 本章主要讲解员工模块,如有问题欢迎在评论区留言

  • 前端仓库nest-study-bacnend

  • nest代码已经放在 gitee demo/v4分支

  • mysql不熟悉的可以看下 前端玩转mysql和Nodejs连接Mysql 这俩篇文章

  • 对Nest语法不熟悉的掘友可以看下Nest文档和Midway文档,搭配服用效果更佳