阅读本文前,需要提前阅读前置内容:
一、Midway 增删改查
二、Midway 增删改查的封装及工具类
三、Midway 接口安全认证
四、Midway 集成 Swagger 以及支持JWT bearer
五、Midway 中环境变量的使用
问题
- 大多数情况,所有实体类都有统一字段,需要抽取实体模型的基类;
- 需要将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>
泛型用法,定义T
为BaseEntity
的子类;
调整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>
泛型用法,定义T
为BaseEntity
的子类;
调整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);
}
}
}
版权所有,转载请注明出处 [码道功成]
网友评论