NestJS 搭建博客系统(十)— 图床模块

2,408 阅读4分钟

NestJS 搭建博客系统(十)— 图床模块

前言

这里做图床模块主要为了节省资源,同时方便图片复用。 基本功能有图片列表和图片新增,上传图片。为了资源复用,可以在上传图片时计算图片 MD5 值对比数据库是否存在,图片模块需要保存图片路径和 md5 值。

开发图床模块

nest g mo modules/picture nest g co modules/picture nest g s modules/picture

Entity DTO VO

// src/modules/picture/entity/picture.entity.ts

import { Common } from 'src/common/entity/common.entity';
import { Entity, Column } from 'typeorm';

@Entity()
export class Picture extends Common{
  // 图片路径
  @Column('text')
  src: string;

  // 文件签名
  @Column('text')
  sign: string;
}
// src/modules/picture/dto/picture.dto.ts

import { IsNotEmpty } from "class-validator";
export class PictureDTO {

  /**
   * 图片路径
   * @example /upload/static/1.png
   */
   @IsNotEmpty({ message: '请输入图片路径' })
   readonly src: string;

}
// src/modules/picture/dto/picture-create.dto.ts

import { PictureDTO } from "./picture.dto";

export class PictureCreateDto extends PictureDTO {

   /**
    * 图片md5
    * @example asdfghjkl
    */
    readonly sign?: string;
}
// src/modules/picture/vo/picture-info.dto.ts

import { SuccessVO } from "src/common/dto/success.dto";
import { PictureDTO } from "../dto/picture.dto";

export class PictureInfoItem extends PictureDTO{}

export class PictureInfoVO {
  info: PictureInfoItem
}

export class PictureInfoSuccessVO extends SuccessVO {
  data: {
    info: PictureInfoItem
  }
} 
// src/modules/picture/vo/picture-list.dto.ts

import { PaginationDTO } from "src/common/dto/pagination.dto";
import { SuccessVO } from "src/common/dto/success.dto";
import { PictureDTO } from "../dto/picture.dto";

export class PictureListItem extends PictureDTO {}

export class PictureListVO {
  list: PictureListItem[]
  pagination: PaginationDTO
}

export class PictureListSuccessVO extends SuccessVO {
  data: {
    list: PictureListItem[]
    pagination: PaginationDTO
  }
} 

引用 entity

// src/modules/picture/picture.modules.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Picture } from './entity/picture.entity';
import { PictureController } from './picture.controller';
import { PictureService } from './picture.service';

@Module({
  imports: [
    TypeOrmModule.forFeature([Picture]),
  ],
  controllers: [PictureController],
  providers: [PictureService]
})
export class PictureModule {}

控制器

import { Controller, Get, Post, Query, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { PageDTO } from 'src/common/dto/Page.dto';
import { PictureService } from './picture.service';
import { PictureInfoSuccessVO, PictureInfoVO } from './vo/picture-info.vo';
import { PictureListSuccessVO, PictureListVO } from './vo/picture-list.vo';

@ApiTags('图床模块')
@Controller('picture')
export class PictureController {
  constructor(
    private pictureService: PictureService
  ) {}

  @ApiOkResponse({ description: '图片列表', type: PictureListSuccessVO })
  @Get('list')
  async getMany(
    @Query() pageDto: PageDTO
  ): Promise<PictureListVO> {
    return await this.pictureService.getMany(pageDto)
  }

  @ApiOkResponse({ description: '上传图片', type: PictureInfoSuccessVO })
  @Post('upload')
  @UseInterceptors(FileInterceptor('file'))
  async upload(
    @UploadedFile() file:any
  ): Promise<PictureInfoVO> {
    console.log('controller', {file})
    return await this.pictureService.upload(file)
  }
}

Service

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { PageDTO } from 'src/common/dto/Page.dto';
import { getPagination } from 'src/utils/index.util';
import { Repository } from 'typeorm';
import { PictureCreateDto } from './dto/picture-create';
import { Picture } from './entity/picture.entity';
import { PictureInfoVO } from './vo/picture-info.vo';
import * as fs from 'fs';
import { encryptFileMD5 } from 'src/utils/cryptogram.util';
import { uploadStaticSrc } from 'src/config/upload/upload.config';

@Injectable()
export class PictureService {
  constructor(
    @InjectRepository(Picture)
    private readonly pictureRepository: Repository<Picture>,
  ) {}

  async getMany(
    pageDto: PageDTO
  ) {
    const { page, pageSize } = pageDto
    const getList = this.pictureRepository
      .createQueryBuilder('picture')
      .select([
        'picture.src',
      ])
      .skip((page - 1) * pageSize)
      .take(pageSize)
      .getManyAndCount()

    const [list, total] = await getList
    const pagination = getPagination(total, pageSize, page)

    return {
      list,
      pagination,
    }
  }

  async create(
    pictureCreateDTO: PictureCreateDto
  ): Promise<PictureInfoVO> {
    const picture = new Picture()
    picture.src = pictureCreateDTO.src
    picture.sign = pictureCreateDTO.sign
    const result = await this.pictureRepository.save(picture)
    return {
      info: result
    }
  }

  async getOneBySign(sign: string) {
    return await this.pictureRepository
      .createQueryBuilder('picture')
      .where('picture.sign = :sign', { sign })
      .getOne()
  }

  async upload(file: any) {
    const { buffer } = file

    const currentSign = encryptFileMD5(buffer)
    const hasPicture = await this.getOneBySign(currentSign)

    if (hasPicture) {
      return {
        info: {
          src: hasPicture.src,
          isHas: true,
        }
      }
    }

    const arr = file.originalname.split('.')
    const fileType = arr[arr.length - 1]
    const fileName = currentSign + '.' + fileType
    fs.writeFileSync(`./upload/${fileName}`, buffer)

    const src = uploadStaticSrc + fileName

    this.create({ src, sign: currentSign })

    return {
      info: {
        src,
        isHas: false
      }
    }
  }

}

其中需要设置静态文件路径

// src/config/upload/upload.config.ts

// 静态文件路径 localhost/static/upload/xxx.jpg
export const uploadStaticSrc = '/static/upload/'
// src/main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { TransformInterceptor } from './interceptor/transform.interceptor';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { uploadStaticSrc } from './config/upload/upload.config';
import { join } from 'path';
import { NestExpressApplication } from '@nestjs/platform-express';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  app.useGlobalPipes(new ValidationPipe())
  app.useGlobalInterceptors(new TransformInterceptor())
  app.useGlobalFilters(new HttpExceptionFilter())

  app.useStaticAssets(join(__dirname, '..', 'upload'), {
    prefix: uploadStaticSrc,
  });

  const options = new DocumentBuilder()
    .setTitle('blog-serve')
    .setDescription('接口文档')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('swagger-doc', app, document);

  await app.listen(3000);
}
bootstrap();

至此,具有查重功能的图床模块已完成,虽然仅有列表查看功能和上传功能,但足以方便重复利用资源

参考

系列