草庐IT

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

码道功臣 2023-03-28 原文

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

一、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 增删改查的封装及工具类的更多相关文章

  1. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  2. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  3. postman接口测试工具-基础使用教程 - 2

    1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,

  4. ruby-on-rails - 有没有一种工具可以在编码时自动保存对文件的增量更改? - 2

    我最喜欢的Google文档功能之一是它会在我工作时不断自动保存我的文档版本。这意味着即使我在进行关键更改之前忘记在某个点进行保存,也很有可能会自动创建一个保存点。至少,我可以将文档恢复到错误更改之前的状态,并从该点继续工作。对于在MacOS(或UNIX)上运行的Ruby编码器,是否有具有等效功能的工具?例如,一个工具会每隔几分钟自动将Gitcheckin我的本地存储库以获取我正在处理的文件。也许我有点偏执,但这点小保险可以让我在日常工作中安心。 最佳答案 虚拟机有些人可能讨厌我对此的回应,但我在编码时经常使用VIM,它具有自动保存功

  5. ruby - 使用 Ruby 开发工具包将文件上传到 Amazon S3 - 2

    我正在尝试上传文件。一个简单的hello.txt。我正在关注文档,但无法将其上传到我的存储桶。#STARTAWSCLIENTs3=Aws::S3::Resource.newbucket=s3.bucket(BUCKET_NAME)begins3.buckets[BUCKET_NAME].objects[KEY].write(:file=>FILE_NAME)puts"Uploadingfile#{FILE_NAME}tobucket#{BUCKET_NAME}."bucket.objects.eachdo|obj|puts"#{obj.key}=>#{obj.etag}"endresc

  6. ruby - 在 StockChart (highchart) 中以编程方式显示柱形图的工具提示 - 2

    我有一个Highstock图表(带有标记和阴影的线条),并且想以编程方式显示一个highstock工具提示,例如,当我选择某个表上的一行(包含图表数据)我想显示相应的highstock工具提示。这可能吗? 最佳答案 股票图表thissolution不起作用:在thisexample你必须更换这个:chart.tooltip.refresh(chart.series[0].data[i]);为此:chart.tooltip.refresh([chart.series[0].points[i]]);解决方案可用here.

  7. ABB-IRB-1200运动学分析MATLAB RVC工具分析+Simulink-Adams联合仿真 - 2

    一、机器人介绍        此处是基于MATLABRVC工具箱,对ABB-IRB-1200型号的微型机械臂进行正逆向运动学分析,并利Simulink工具实现对机械臂进行具有动力学参数的末端轨迹规划仿真,最后根据机械模型设计Simulink-Adams联合仿真。 图1.ABBIRB 1200尺寸参数示意图ABBIRB 1200提供的两种型号广泛适用于各作业,且两者间零部件通用,两种型号的工作范围分别为700 mm 和 900 mm,大有效负载分别为 7 kg 和5 kg。 IRB 1200 能够在狭小空间内能发挥其工作范围与性能优势,具有全新的设计、小型化的体积、高效的性能、易于集成、便捷的接

  8. Ruby & Syslog & 自定义工具 - 2

    我是syslog的新手。我们决定使用系统日志来跟踪Rails应用程序中的一些特殊事件。问题是我不想使用默认的/var/log/system.log文件,而是使用自定义文件,例如/var/log/myapp_events.log.我看到我必须像这样在/etc/syslog.conf中定义我自己的设施:myapp_events.*/var/log/myapp_events.log重新启动syslogd后,我发现我可以直接在bash控制台中使用它:syslog-s-kFacilitymyapp_eventsMessage"thisismymessage"该消息按预期出现在/var/log/m

  9. ruby-on-rails - 如何在 Ruby 中封装包含的模块方法? - 2

    我希望能够在包含该模块的类无法访问的模块中拥有方法。给定以下示例:classFooincludeBardefdo_stuffcommon_method_nameendendmoduleBardefdo_stuffcommon_method_nameendprivatedefcommon_method_name#blahblahendend我希望Foo.new.do_stuff爆炸,因为它试图访问模块试图对其隐藏的方法。不过,在上面的代码中,Foo.new.do_stuff可以正常工作:(有没有办法在Ruby中实现我想做的事情?更新-真正的代码classPlace"Place"}has_

  10. ruby - 使用 Gatling 作为集成测试工具 - 2

    目前我有一小套针对我的网络服务器运行的集成测试,它发出请求并断言一些关于响应应该是什么的假设。这些是用Ruby编写的,生成http请求。我一直在看Gatling作为压力测试工具,但我想知道它是否也可以用于集成测试。这样,所有端点请求都可以在压力测试和集成测试中重复使用。我可能在这里失去了一些东西,因为没有RSpec的BDD,但不必两次创建相同的测试。有没有人有这样使用gatling的经验? 最佳答案 您可以使用AssertionAPI并设置验收标准。但是,Gatling不是浏览器,不会运行/测试您的Javascript,因此这种方法

随机推荐