美文网首页程序员
二、Midway 增删改查的封装及工具类

二、Midway 增删改查的封装及工具类

作者: 码道功臣 | 来源:发表于2022-07-18 19:25 被阅读0次

    阅读本文前,需要提前阅读前置内容:

    一、Midway 增删改查
    二、Midway 增删改查的封装及工具类
    三、Midway 接口安全认证
    四、Midway 集成 Swagger 以及支持JWT bearer
    五、Midway 中环境变量的使用

    样例源码
    DEMO LIVE

    问题

    • 大多数情况,所有实体类都有统一字段,需要抽取实体模型的基类;
    • 需要将Service的基本操作封装起来;
    • 需要将Controller的基本操作封装起来

    抽取Entity基类

    • 创建目录common;
    • 创建基类src/common/BaseEntity.ts;
    // src/common/BaseEntity.ts
    import { Column, CreateDateColumn, PrimaryColumn, UpdateDateColumn } from 'typeorm';
    
    export class BaseEntity {
      @PrimaryColumn({ type: 'bigint' })
      id: number;
    
      @Column({ type: 'bigint' })
      updaterId: number;
    
      @Column({ type: 'bigint' })
      createrId: number;
    
      @CreateDateColumn()
      createTime: Date;
    
      @UpdateDateColumn()
      updateTime: Date;
    }
    
    • 调整实体类src/entity/user.ts;

    继承BaseEntity,并删除user.ts中的通用字段。

    // src/entity/user.ts
    import { EntityModel } from '@midwayjs/orm';
    import { Column } from 'typeorm';
    import { BaseEntity } from '../common/BaseEntity';
    
    @EntityModel('user')
    export class User extends BaseEntity {
      @Column({ length: 100, nullable: true })
      avatarUrl: string;
    
      @Column({ length: 20, unique: true })
      username: string;
    
      @Column({ length: 200 })
      password: string;
    
      @Column({ length: 20 })
      phoneNum: string;
    
      @Column()
      regtime: Date;
    
      @Column({ type: 'int', default: 1 })
      status: number;
    }
    

    抽取Service基类

    创建基类src/common/BaseService.ts;

    // src/common/BaseService.ts
    import { In, Repository } from 'typeorm';
    import { BaseEntity } from './BaseEntity';
    import { FindOptionsWhere } from 'typeorm/find-options/FindOptionsWhere';
    
    export abstract class BaseService<T extends BaseEntity> {
    
      abstract getModel(): Repository<T>;
    
      async save(o: T) {
        if (!o.id) o.id = new Date().getTime();
        return this.getModel().save(o);
      }
    
      async delete(id: number) {
        return this.getModel().delete(id);
      }
    
      async findById(id: number): Promise<T> {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        return this.getModel().findOneBy({ id });
      }
    
      async findByIds(ids: number[]): Promise<T[]> {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        return this.getModel().findBy({ id: In(ids) });
      }
    
      async findOne(where: FindOptionsWhere<T>): Promise<T> {
        return this.getModel().findOne({ where });
      }
    
    }
    
    • 基类定义为抽象类abstract,并添加抽象接口abstract getModel()
    • <T extends BaseEntity>泛型用法,定义TBaseEntity的子类;

    调整src/service/user.service.ts;

    import { Provide } from '@midwayjs/decorator';
    import { User } from '../eneity/user';
    import { InjectEntityModel } from '@midwayjs/orm';
    import { Repository } from 'typeorm';
    import { BaseService } from '../common/BaseService';
    
    @Provide()
    export class UserService extends BaseService<User> {
    
      @InjectEntityModel(User)
      model: Repository<User>;
    
      getModel(): Repository<User> {
        return this.model;
      }
    
    }
    
    • 添加继承UserService extends BaseService<User>;
    • 实现接口getModel(),并返回Repository;

    抽取Controller基类

    创建基类src/common/BaseController.ts;

    // src/common/BaseController.ts
    import { BaseService } from './BaseService';
    import { BaseEntity } from './BaseEntity';
    import { Body, Post, Query } from '@midwayjs/decorator';
    
    /**
     * Controller基础类,由于类继承不支持装饰类@Post、@Query、@Body等,
     * 所以这里的装饰类不生效,否则实现类就不需要再写多余代码了,
     * 这里保留在这里,以备以后可能会支持继承的装饰类
     */
    export abstract class BaseController<T extends BaseEntity> {
    
      abstract getService(): BaseService<T>;
    
      @Post('/create')
      async create(@Body() body: T): Promise<T> {
        return this.getService().save(body);
      }
    
      @Post('/delete')
      async delete(@Query('id') id: number): Promise<boolean> {
        await this.getService().delete(id);
        return true;
      }
    
      @Post('/update')
      async update(@Body() body: T): Promise<T> {
        return this.getService().save(body);
      }
    
      @Post('/findById')
      async findById(@Query('id') id: number): Promise<T> {
        return this.getService().findById(id);
      }
    
      @Post('/findByIds')
      async findByIds(@Query('ids') ids: number[]): Promise<T[]> {
        return this.getService().findByIds(ids);
      }
    
    }
    
    • 基类定义为抽象类abstract,并添加抽象接口abstract getService()
    • <T extends BaseEntity>泛型用法,定义TBaseEntity的子类;

    调整src/controller/user.controller.ts;

    // src/controller/user.controller.ts
    import { Inject, Controller, Query, Post, Body } from '@midwayjs/decorator';
    import { User } from '../eneity/user';
    import { UserService } from '../service/user.service';
    import { BaseController } from '../common/BaseController';
    import { BaseService } from '../common/BaseService';
    
    @Controller('/api/user')
    export class UserController extends BaseController<User> {
    
      @Inject()
      userService: UserService;
    
      getService(): BaseService<User> {
        return this.userService;
      }
    
      @Post('/create', { description: '创建' })
      async create(@Body() user: User): Promise<User> {
        Object.assign(user, {
          id: new Date().getTime(),
          regtime: new Date(),
          updaterId: 1,
          createrId: 1,
        });
        return super.create(user);
      }
    
      @Post('/findById', { description: '通过主键查找' })
      async findById(@Query('id') id: number): Promise<User> {
        return super.findById(id);
      }
    
      @Post('/delete', { description: '删除' })
      async delete(@Query('id') id: number): Promise<boolean> {
        return super.delete(id);
      }
    
    }
    
    • 添加继承UserController extends BaseController
    • 实现抽象接口getService()
    • 调用基类方法,使用super.xxx()

    运行单元测试

    >npm run test
    
    Test Suites: 2 passed, 2 total
    Tests:       4 passed, 4 total
    Snapshots:   0 total
    Time:        10.686 s
    

    统一返回结果处理

    中间件

    web中间件是在控制器调用之前之后调用的函数方法,我们可以利用中间件在接口执行前或者后,加一些逻辑。
    比如:统一返回格式、接口鉴权。

    统一接口状态、异常码

    • 添加src/common/ErrorCode.ts;
    // src/common/ErrorCode.ts
    export class ErrorCode {
      /**
       * 100000 正常
       */
      static OK = 100000;
      /**
       * 400000-500000 平台异常
       */
      static SYS_ERROR = 400000;
      /**
       * 50000 未知异常
       */
      static UN_ERROR = 500000;
      /**
       * 60000-69999 基本的业务异常
       */
      static BIZ_ERROR = 600000;
    }
    
    • 添加通用异常类src/common/CommonException.ts;
    // src/common/CommonException.ts
    import { MidwayError } from '@midwayjs/core';
    
    export class CommonException extends MidwayError {
      code: number;
      msg: string;
      data: any;
      constructor(code: number, msg: string) {
        super(msg, code.toString());
        this.code = code;
        this.msg = msg;
      }
    }
    

    使用中间件统一接口返回数据格式

    添加中间件src/middleware/format.middleware.ts

    // src/middleware/format.middleware.ts
    import { IMiddleware } from '@midwayjs/core';
    import { Middleware } from '@midwayjs/decorator';
    import { NextFunction, Context } from '@midwayjs/koa';
    import { ErrorCode } from '../common/ErrorCode';
    
    /**
     * 对接口返回的数据统一包装
     */
    @Middleware()
    export class FormatMiddleware implements IMiddleware<Context, NextFunction> {
      resolve() {
        return async (ctx: Context, next: NextFunction) => {
          const result = await next();
          return { code: ErrorCode.OK, msg: 'OK', data: result };
        };
      }
    
      match(ctx) {
        return ctx.path.indexOf('/api') === 0;
      }
    
      static getName(): string {
        return 'API_RESPONSE_FORMAT';
      }
    }
    
    • @Middleware()标识此类是一个中间件;
    • match(ctx)方法确定哪些路径会被拦截;

    详细的中间件使用说明见:http://www.midwayjs.org/docs/middleware

    注册中间件

    注册中间件,需要修改src/configuration.ts

    import { Configuration, App } from '@midwayjs/decorator';
    import * as koa from '@midwayjs/koa';
    import * as validate from '@midwayjs/validate';
    import * as info from '@midwayjs/info';
    import { join } from 'path';
    import { ReportMiddleware } from './middleware/report.middleware';
    import * as orm from '@midwayjs/orm';
    import { FormatMiddleware } from './middleware/format.middleware';
    
    @Configuration({
      imports: [
        orm, // 引入orm组件
        koa,
        validate,
        {
          component: info,
          enabledEnvironment: ['local'],
        },
      ],
      importConfigs: [join(__dirname, './config')],
    })
    export class ContainerLifeCycle {
      @App()
      app: koa.Application;
    
      async onReady() {
        // 注册中间件 FormatMiddleware
        this.app.useMiddleware([FormatMiddleware, ReportMiddleware]);
      }
    }
    

    Postman查看返回结果

    此时返回结果已经被重新包装了。


    返回结果包装

    异常处理

    统一的异常处理使用异常过滤器,可以在这里进行异常的封装处理。

    • 创建或者修改异常过滤器src/filter/default.filter.ts;
    // src/filter/default.filter.ts
    import { Catch } from '@midwayjs/decorator';
    import { Context } from '@midwayjs/koa';
    import { ErrorCode } from '../common/ErrorCode';
    
    @Catch()
    export class DefaultErrorFilter {
    
      async catch(err: Error, ctx: Context) {
        return { code: ErrorCode.UN_ERROR, msg: err.message };
      }
    
    }
    
    • 创建或者修改异常过滤器src/filter/notfound.filter.ts;
    // src/filter/notfound.filter.ts
    import { Catch } from '@midwayjs/decorator';
    import { httpError, MidwayHttpError } from '@midwayjs/core';
    import { Context } from '@midwayjs/koa';
    
    @Catch(httpError.NotFoundError)
    export class NotFoundFilter {
    
      async catch(err: MidwayHttpError, ctx: Context) {
        // 404 错误会到这里
        ctx.redirect('/404.html');
      }
    
    }
    
    • 注册异常过滤器;
    // src/configuration.ts
    import { Configuration, App } from '@midwayjs/decorator';
    import * as koa from '@midwayjs/koa';
    import * as validate from '@midwayjs/validate';
    import * as info from '@midwayjs/info';
    import { join } from 'path';
    import { ReportMiddleware } from './middleware/report.middleware';
    import * as orm from '@midwayjs/orm';
    import { FormatMiddleware } from './middleware/format.middleware';
    import { NotFoundFilter } from './filter/notfound.filter';
    import { DefaultErrorFilter } from './filter/default.filter';
    
    @Configuration({
      imports: [
        orm, // 引入orm组件
        koa,
        validate,
        {
          component: info,
          enabledEnvironment: ['local'],
        },
      ],
      importConfigs: [join(__dirname, './config')],
    })
    export class ContainerLifeCycle {
    
      @App()
      app: koa.Application;
    
      async onReady() {
        this.app.useMiddleware([FormatMiddleware, ReportMiddleware]);
        // 注册异常过滤器
        this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
      }
    
    }
    
    • 使用Postman验证(创建用户,输入一个过长的用户名);


      创建用户

    单元测试

    由于调整了返回值,此时单元测试会报错,我们需要调整下单元。修改test/controller/user.test.ts

    o = result.body;
    # 改为
    o = result.body.data;
    
    >npm run test
    
    Test Suites: 2 passed, 2 total
    Tests:       4 passed, 4 total
    Snapshots:   0 total
    Time:        6.525 s, estimated 9 s
    
    

    工具类

    问题&需求

    • 数据库主键需要是一个有序的、全局唯一的长整形;
    • 用户的密码需要加密存储,能够验证密码;
    • 业务异常需要需要返回给前端,这里使用断言工具

    主键生成器

    我们使用Snowflake主键生成算法。
    其优点是:高性能,低延迟;独立的应用;按时间有序。
    缺点是:需要独立的开发和部署。
    我们这里把算法迁移到本地,测试开发没有问题,生产使用需要配置数据中心和服务器。

    • 创建工具目录utils;
    • 创建工具类src/utils/Snowflake.ts;
    // src/utils/Snowflake.ts
    import { Provide } from '@midwayjs/decorator';
    
    /**
     * Snowflake主键生成算法
     * 完整的算法是生成的ID长度为20位
     * 但是由于js最大值9007199254740991,再多就会溢出,再多要特殊处理。
     * 所以这里设置长度为16位id。将数据中心位调小到1位,将服务器位调小到1位,将序列位调小到10位
     * 这意味着最多支持两个数据中心,每个数据中心最多支持两台服务器
     */
    @Provide('idGenerate')
    export class SnowflakeIdGenerate {
      private twepoch = 0;
      private workerIdBits = 1;
      private dataCenterIdBits = 1;
      private maxWrokerId = -1 ^ (-1 << this.workerIdBits); // 值为:1
      private maxDataCenterId = -1 ^ (-1 << this.dataCenterIdBits); // 值为:1
      private sequenceBits = 10;
      private workerIdShift = this.sequenceBits; // 值为:10
      private dataCenterIdShift = this.sequenceBits + this.workerIdBits; // 值为:11
      // private timestampLeftShift =
      //   this.sequenceBits + this.workerIdBits + this.dataCenterIdBits; // 值为:12
      private sequenceMask = -1 ^ (-1 << this.sequenceBits); // 值为:4095
      private lastTimestamp = -1;
      private workerId = 1; //设置默认值,从环境变量取
      private dataCenterId = 1;
      private sequence = 0;
    
      constructor(_workerId = 0, _dataCenterId = 0, _sequence = 0) {
        if (this.workerId > this.maxWrokerId || this.workerId < 0) {
          throw new Error('config.worker_id must max than 0 and small than maxWrokerId-[' + this.maxWrokerId + ']');
        }
        if (this.dataCenterId > this.maxDataCenterId || this.dataCenterId < 0) {
          throw new Error(
            'config.data_center_id must max than 0 and small than maxDataCenterId-[' + this.maxDataCenterId + ']',
          );
        }
        this.workerId = _workerId;
        this.dataCenterId = _dataCenterId;
        this.sequence = _sequence;
      }
    
      private timeGen = (): number => {
        return Date.now();
      };
    
      private tilNextMillis = (lastTimestamp): number => {
        let timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
          timestamp = this.timeGen();
        }
        return timestamp;
      };
    
      private nextId = (): number => {
        let timestamp: number = this.timeGen();
        if (timestamp < this.lastTimestamp) {
          throw new Error('Clock moved backwards. Refusing to generate id for ' + (this.lastTimestamp - timestamp));
        }
        if (this.lastTimestamp === timestamp) {
          this.sequence = (this.sequence + 1) & this.sequenceMask;
          if (this.sequence === 0) {
            timestamp = this.tilNextMillis(this.lastTimestamp);
          }
        } else {
          this.sequence = 0;
        }
        this.lastTimestamp = timestamp;
        // js 最大值 9007199254740991,再多就会溢出
        // 超过 32 位长度,做位运算会溢出,变成负数,所以这里直接做乘法,乘法会扩大存储
        const timestampPos = (timestamp - this.twepoch) * 4096;
        const dataCenterPos = this.dataCenterId << this.dataCenterIdShift;
        const workerPos = this.workerId << this.workerIdShift;
        return timestampPos + dataCenterPos + workerPos + this.sequence;
      };
    
      generate = (): number => {
        return this.nextId();
      };
    }
    

    密码工具

    安装组件

    >npm i bcryptjs --save
    

    添加工具类src/utils/PasswordEncoder.ts

    // src/utils/PasswordEncoder.ts
    const bcrypt = require('bcryptjs');
    
    /**
     * 加密。加上前缀{bcrypt},为了兼容多种加密算法,这里暂时只实现bcrypt算法
     */
    export function encrypt(password) {
      const salt = bcrypt.genSaltSync(5);
      const hash = bcrypt.hashSync(password, salt, 64);
      return '{bcrypt}' + hash;
    }
    
    /**
     * 解密
     */
    export function decrypt(password, hash) {
      if (hash.indexOf('{bcrypt}') === 0) {
        hash = hash.slice(8);
      }
      return bcrypt.compareSync(password, hash);
    }
    

    断言工具

    // src/common/Assert.ts
    import { CommonException } from './CommonException';
    
    export class Assert {
      /**
       * 不为空断言
       */
      static notNull(obj: any, errorCode: number, errorMsg: string) {
        if (!obj) {
          throw new CommonException(errorCode, errorMsg);
        }
      }
    
      /**
       * 空字符串断言
       */
      static notEmpty(obj: any, errorCode: number, errorMsg: string) {
        if (!obj || '' === obj.trim()) {
          throw new CommonException(errorCode, errorMsg);
        }
      }
    
      /**
       * 布尔断言
       */
      static isTrue(expression: boolean, errorCode: number, errorMsg: string) {
        if (!expression) {
          throw new CommonException(errorCode, errorMsg);
        }
      }
    
    }
    

    版权所有,转载请注明出处 [码道功成]

    相关文章

      网友评论

        本文标题:二、Midway 增删改查的封装及工具类

        本文链接:https://www.haomeiwen.com/subject/ncydirtx.html