Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【一次nest服务开发&上线】 #3

Open
bitjian opened this issue Dec 13, 2023 · 0 comments
Open

【一次nest服务开发&上线】 #3

bitjian opened this issue Dec 13, 2023 · 0 comments
Labels
node node知识

Comments

@bitjian
Copy link
Owner

bitjian commented Dec 13, 2023

记录一次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的模块
image
在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 }; // 解构入参,并给page默认值
    if(pageSize > 100) {    // pageSize限制一下,不然我怕我的服务器扛不住
      pageSize = 100
    }
   // 通过keyword长度,去选择使用什么样的查询,全文索引对匹配的字段长度有限制,
   // 我目前设置的是4,长度4以下的就走like查询,否则走全文索引搜索
      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` 使用nest的 interceptor 拦截器和文件Pipe 管道来处理上传文件 
  @UseInterceptors(FileInterceptor('file'))
  // 通过 自定义的validation 对文件大小和类型进行验证处理
  async uploadExcel(@UploadedFile(FileSizeValidationPipePipe) file: Express.Multer.File) {
      // ...省略代码
     // 通过 xlsx解析文件buffer,来获取xlsx数据,并插入数据库
      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()
// ...省略代码

跨域处理

主要记录

  • 带cookie的跨域处理

src/main.ts

  // 不支持带cookie的跨域
  // const app = await NestFactory.create<NestExpressApplication>(AppModule, {cros: true});
 // 带cookie跨域请求 access-control-allow-Origin/Methods/Headers 不能为 *
  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模块

nest g module 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'
        })
      ]
  })
@bitjian bitjian added the node node知识 label Dec 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
node node知识
Projects
None yet
Development

No branches or pull requests

1 participant