写完用户模块的后端代码,我们继续来写图书模块。
这个模块的接口就是增删改查:
我们来添加下 book 模块:
nest g resource book
不用生成 crud 代码了,我们自己来写就行。
该下 BookController,添加一些路由:
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { BookService } from './book.service';
import { CreateBookDto } from './dto/create-book.dto';
import { UpdateBookDto } from './dto/update-book.dto';
@Controller('book')
export class BookController {
constructor(private readonly bookService: BookService) {}
@Get('list')
async list() {
return this.bookService.list();
}
@Get(':id')
async findById(@Param('id') id: string) {
return this.bookService.findById(+id);
}
@Post('create')
async create(@Body() createBookDto: CreateBookDto) {
return this.bookService.create(createBookDto);
}
@Put('update')
async update(@Body() updateBookDto: UpdateBookDto) {
return this.bookService.update(updateBookDto);
}
@Delete('delete/:id')
async delete(@Param('id') id: string) {
return this.bookService.delete(+id);
}
}
我们新增了 /book/list、/book/:id 这两个查询的接口。
新增了 /book/create、/book/update、/book/delete/:id 这三个增删改的接口。
然后创建用到的 dto:
这里 CreateBookDto 和 UpdateBookDto 都根据原型图来确定字段:
book/dto/create-book.dto.ts
import { IsNotEmpty } from "class-validator";
export class CreateBookDto {
@IsNotEmpty({ message: '书名不能为空' })
name: string;
@IsNotEmpty({ message: '作者不能为空' })
author: string;
@IsNotEmpty({ message: '描述不能为空' })
description: string;
@IsNotEmpty({ message: '封面不能为空' })
cover: string;
}
book/dto/update-book.dto.ts
import { IsNotEmpty } from "class-validator";
export class UpdateBookDto {
@IsNotEmpty({ message: 'id 不能为空' })
id: number;
@IsNotEmpty({ message: '书名不能为空' })
name: string;
@IsNotEmpty({ message: '作者不能为空' })
author: string;
@IsNotEmpty({ message: '描述不能为空' })
description: string;
@IsNotEmpty({ message: '封面不能为空' })
cover: string;
}
更新的时候还要传入书的 id
然后在 BookService 里实现这些方法:
import { UpdateBookDto } from './dto/update-book.dto';
import { CreateBookDto } from './dto/create-book.dto';
import { Injectable } from '@nestjs/common';
@Injectable()
export class BookService {
list() {
}
findById(id: number) {
}
create(createBookDto: CreateBookDto) {
}
update(updateBookDto: UpdateBookDto) {
}
delete(id: number) {
}
}
这里我们需要用到 DbModule
在 BookModule 里引入下:
感受到封装 DbModule 为动态模块的意义了么?用的时候可以传入不同的参数,动态改变模块的内容。
接下来注入 dbService,实现下 service 的方法:
import { UpdateBookDto } from './dto/update-book.dto';
import { CreateBookDto } from './dto/create-book.dto';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { DbService } from 'src/db/db.service';
import { Book } from './entities/book.entity';
function randomNum() {
return Math.floor(Math.random() * 1000000);
}
@Injectable()
export class BookService {
@Inject()
dbService: DbService;
async list() {
const books: Book[] = await this.dbService.read();
return books;
}
async findById(id: number) {
const books: Book[] = await this.dbService.read();
return books.find(book => book.id === id);
}
async create(createBookDto: CreateBookDto) {
const books: Book[] = await this.dbService.read();
const book = new Book();
book.id = randomNum();
book.author = createBookDto.author;
book.name = createBookDto.name;
book.description = createBookDto.description;
book.cover = createBookDto.cover;
books.push(book);
await this.dbService.write(books);
return book;
}
async update(updateBookDto: UpdateBookDto) {
const books: Book[] = await this.dbService.read();
const foundBook = books.find(book => book.id === updateBookDto.id);
if(!foundBook) {
throw new BadRequestException('该图书不存在');
}
foundBook.author = updateBookDto.author;
foundBook.cover = updateBookDto.cover;
foundBook.description = updateBookDto.description;
foundBook.name = updateBookDto.name;
await this.dbService.write(books);
return foundBook;
}
async delete(id: number) {
const books: Book[] = await this.dbService.read();
const index = books.findIndex(book => book.id === id);
if(index !== -1) {
books.splice(index, 1);
await this.dbService.write(books);
}
}
}
虽然代码比较多,但是都比较简单,就是读取文件里 books 的内容,做下增删改,然后再写入文件。
其中,新增时的 id 是用随机数实现的。
创建用到到的 entities/book.entity.ts
export class Book {
id: number;
name: string;
author: string;
description: string;
cover: string;
}
测试下:
先新增三本书:
在 books.json 文件里保存了这三本书的数据:
然后试下查询单本书的信息:
再试下修改:
修改成功。
再测试下删除:
也没问题。
这样,图书的增删改查就都完成了。
还有一个上传封面的接口:
安装用到的包:
npm install --save multer
npm install -save-dev @types/multer
然后在 BookController 添加一个 /book/upload 接口:
@Post('upload')
@UseInterceptors(FileInterceptor('file', {
dest: 'uploads',
storage: storage,
limits: {
fileSize: 1024 * 1024 * 3
},
fileFilter(req, file, callback) {
const extname = path.extname(file.originalname);
if(['.png', '.jpg', '.gif'].includes(extname)) {
callback(null, true);
} else {
callback(new BadRequestException('只能上传图片'), false);
}
}
}))
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log('file', file);
return file.path;
}
通过 FileInterceptor 的拦截器来解析请求里的 file 字段,第二个参数是一些 options:
dest 是保存文件的目录,limits 是文件大小限制,限制为 3 M,fileFilter 限制扩展名只能是图片。
然后创建自定义 storage。
my-file-storage.ts
import * as multer from "multer";
import * as fs from 'fs';
const storage = multer.diskStorage({
destination: function (req, file, cb) {
try {
fs.mkdirSync('uploads');
}catch(e) {}
cb(null, 'uploads')
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9) + '-' + file.originalname
cb(null, uniqueSuffix)
}
});
export { storage };
这里指定了保存的目录为 uploads,文件名为时间戳-随机数-文件名
的格式。
在 postman 里测试下:
切换 body 格式为 form-data,添加一个 file 字段,然后选择一个文件:
可以看到,服务端接收到了文件,并且返回了文件路径。
当你传的不是图片的时候,会返回 400 响应:
此外,还要把 uploads 目录设置为静态文件目录:
app.useStaticAssets(join(__dirname, '../uploads'), {prefix: '/uploads'});
就能直接访问了:
这样,上传接口就完成了。
然后上传完文件,把返回的文件路径作为 cover 字段的值传上去就可以了:
案例代码上传了小册仓库
总结
这节我们实现了图书模块的接口,包括列表、详情、创建、更新、删除、上传文件。
创建了用到的 dto,并且添加了 class-validator 的校验规则的装饰器。
用 multer 实现了文件上传,并做了自定义文件存储路径。
这样,我们后端的接口就都写完了。