记录一次nest服务开发&上线
年底了,高中同学被老板安排去核对两个表格,一个表格相当于是字典, 另一个表格会提供一个抽象的店名, 去字典表里查询该店名的店铺编号,一开始想这简单,写个脚本分分钟给他弄好,结果啪啪啪打脸,要查的店铺名实在是太抽象了,可能是字典表里三列的某一列数据。
shop_id |
shop_name |
shop_addr |
province |
sys_name |
234432 |
天M超市神墩三路店 |
湖北武汉市江夏区神墩三路 |
武汉市 |
天M超市 |
shop_id |
shop_name |
要填写的code |
江夏神墩路社区超市 |
想了一下还是给他弄一个管理后台,让他自己去搜索查找吧,搜索多列字段听说用全文索引
会好很多,正好也学习一下这方面的知识,顺带练练一下刚学的nest。
开发工具
- 后台服务:nest
- 数据库:mysql
- 前端: TDesign Starter
- 一台服务器:docker
装好 nginx, mysql
- 一个cos桶 + 域名
后台服务
在数据库创建表
在服务器上我已经通过docker搭建了一个mysql,远程连接上去执行下面语句
CREATE TABLE `fulltext_test` ( `id` int (11) NOT NULL AUTO_INCREMENT, `shop_name` varchar(255) NOT NULL COMMENT '店名', `province` varchar(255) DEFAULT NULL COMMENT '省份', `sys_name` varchar(255) DEFAULT NULL COMMENT '系统名', `shop_id` varchar(255) DEFAULT NULL COMMENT '店铺id', `shop_addr` varchar(255) DEFAULT NULL COMMENT '店铺详细地址', PRIMARY KEY (`id`), FULLTEXT KEY `index_content_tag` (`shop_name`, `sys_name`, `shop_addr`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8
|
创建项目
使用脚手架创建项目
npm install -g @nestjs/cli nest new zizi-mgr-serve (你的项目名)
|
通过cli创建curd的 api模块
这个时候就已经有一个nest开发框架了,通过cli创建一个可以curd的 api模块
nest g resource excelFind
|
会在src目录生成一个excel-find的模块
在src/excel-find/entities/excel-find.entity.ts 编写对应的数据库实例对象,熟悉一下注解语法
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity({ name: 'fulltext_test', }) class ExcelFind { @PrimaryGeneratedColumn() id: number; @Column({ comment: '店铺名称', }) shop_name: string; } export {ExcelFind}
|
连接数据库
通过typeorm在 src/app.module.ts 连接数据库,首先安装依赖
npm install --save @nestjs/typeorm typeorm mysql2
|
src/app.module.ts
@Module({ imports: [ ExcelFindModule, TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'root', database: 'test', synchronize: true, logging: true, entities: [ExcelFindModule], poolSize: 10, connectorPackage: 'mysql2', extra: { authPlugin: 'sha256_password', }, }) })
|
服务开发
查询getShopList
主要记录
- mysql的全文索引通配符匹配
- 参数里特殊字符在全文索引匹配报错问题
创建一个post的路由,通过@Body
获取对应的参数对象,主要逻辑在service层处理
src/excel-find/excel-find.controller.ts
@Post('getShopList') async getShopList(@Body() queryExcelFindDto: QueryExcelFindDto) { const ret = await this.excelFindService.getShopList(queryExcelFindDto); return { code: 0, data:ret } }
|
src/excel-find/excel-find.service.ts
代码就不全部贴了,主要记录一下遇到的坑点
async getShopList(params: QueryExcelFindDto, isLike = false) { const { keyword = '', province, sys_name, page: { pageNum = 1 }, } = { page: {}, ...params }; let { page: { pageSize = 10 } } = { page: {}, ...params }; if(pageSize > 100) { pageSize = 100 } let sqlStr = `select * from fulltext_test `; let whereStr = keyword.length >= 4 && isLike === false ? ` where match(shop_name,shop_addr,sys_name) against(? in boolean mode)` : ' where 1=1'; const db_params = []; if (keyword.length >= 4 && isLike === false) { db_params.push(keyword.replaceAll(/-/g, '')) } else { whereStr += ' and (shop_name like ? or shop_addr like ? or sys_name like ?)'; db_params.push(...[`%${keyword}%`, `%${keyword}%`, `%${keyword}%`]) } } const [ret, count] = await Promise.all([this.manager.query(sqlStr, db_params), this.manager.query(`select count(*) as total from fulltext_test ${whereStr}`, db_params)])
|
解析上传文件的数据插入数据库
主要记录
- nest对上传文件的验证与处理
- nest验证器的使用
- 高版本依赖包esmodule导出,引入打包会有问题
- 上传文件安全校验问题
- 通过typeorm的queryBuilder进行sql处理
对上传文件处理需要安装依赖,然后通过拦截器和管道,便可以获取文件对象
npm install -D @types/multer
|
src/excel-find/excel-find.controller.ts
@Post('uploadExcel')
@UseInterceptors(FileInterceptor('file'))
async uploadExcel(@UploadedFile(FileSizeValidationPipePipe) file: Express.Multer.File) { const xlsxData = xlsx.parse(file.buffer) dataList = xlsxData[0].data.filter(Boolean) headKey = dataList.splice(0, 1)[0] const rowCount = await this.excelFindService.insertData(headKey, dataList)
}
|
通过验证器对参数进行校验
创建一个validationPipe,通过filetype依赖包对文件来下进行校验
filetype会获取文件的二进制类型,比较靠谱,可以防止文件伪装
不过这里filetype高版本引入会有问题,因为打包变成CommonJS引入,提示不能使用esmodule导出的包…
nest g pipe file-size-validation-pipe --no-spec --flat
|
src/file-size-validation-pipe.pipe
@Injectable() export class FileSizeValidationPipePipe implements PipeTransform { async transform(value: any, metadata: ArgumentMetadata) { if(value.size > 5 * 1024 * 1024) { throw new HttpException('文件大于 5m', HttpStatus.BAD_REQUEST); } const typeObj = await filetype.fromBuffer(value.buffer) const allowType = ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'] if(!typeObj || !allowType.includes(typeObj.mime)) throw new HttpException('文件类型不正确', HttpStatus.BAD_REQUEST) return value; } }
|
src/excel-find.service
这里insert().into第一个参数好像不能使用resource模块生成的entity
const { raw } = await queryBuilder.insert().into('fulltext_test', headKey).values(dataValues).execute()
|
跨域处理
主要记录
src/main.ts
app.enableCors({ origin: 'http://localhost:3002', methods: 'GET,HEAD,POST,DELETE', allowedHeaders: 'Content-Type, Accept', credentials: true, });
|
日志处理
主要记录
- winston处理日志
- 动态模块
首先安装依赖npm install --save winston dayjs chalk@4
|
创建一个winston模块
创建有一个winston类
src/winston/MyLogger.ts
import { ConsoleLogger, LoggerService, LogLevel } from '@nestjs/common'; import * as chalk from 'chalk'; import * as dayjs from 'dayjs'; import { createLogger, format, Logger, transports } from 'winston'; export class MyLogger implements LoggerService { private logger: Logger; constructor(options) { this.logger = createLogger(options); } log(message: string, context: string) { const time = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss'); this.logger.log('info', message, { context, time }); } error(message: string, context: string) { const time = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss'); this.logger.log('info', message, { context, time }); } warn(message: string, context: string) { const time = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss'); this.logger.log('info', message, { context, time }); } }
|
封装成一个动态模块,导出模块
src/winston/winston.module.ts
import { DynamicModule, Global, Module } from '@nestjs/common'; import { LoggerOptions, createLogger } from 'winston'; import { MyLogger } from './MyLogger'; export const WINSTON_LOGGER_TOKEN = 'WINSTON_LOGGER'; @Global() @Module({}) export class WinstonModule { public static forRoot(options: LoggerOptions): DynamicModule { return { module: WinstonModule, providers: [ { provide: WINSTON_LOGGER_TOKEN, useValue: new MyLogger(options) } ], exports: [ WINSTON_LOGGER_TOKEN ] }; } }
|
在app.module.ts初始化winston配置
src/app.module.ts
WinstonModule.forRoot({ level: 'debug', transports: [ new transports.Console({ format: format.combine( format.colorize(), format.printf(({context, level, message, time}) => { const appStr = chalk.green(`[NEST]`); const contextStr = chalk.yellow(`[${context}]`); return `${appStr} ${time} ${level} ${contextStr} ${message} `; }) ), }), new transports.File({ format: format.combine( format.timestamp(), format.json() ), filename: `${dayjs(Date.now()).format('YYYY-MM')}.log`, dirname: 'log' }) ] })
|