Skip to content

@vup/nest-upload 上传模块

一个可在 monorepo 内复用的 NestJS 上传模块,默认使用本地存储,提供 Service + Controller 的最小可用封装。支持场景化配置、文件类型限制、大小限制等功能。

技术栈

  • NestJS - Node.js 企业级框架
  • TypeORM - TypeScript ORM
  • Multer - 文件上传中间件

快速开始

安装

在 NestJS 项目中,该包通常已经通过 monorepo workspace 可用,无需单独安装。

基础使用

在业务模块中引入并配置:

ts
import { Module } from '@nestjs/common';
import { UploadModule } from '@vup/nest-upload';

@Module({
  imports: [
    UploadModule.forRoot({
      uploadDir: 'uploads',
      baseUrl: 'http://localhost:3000/uploads',
    }),
  ],
})
export class AppModule {}

上传文件

配置完成后,即可使用上传接口:

bash
# 上传文件
POST /upload
Content-Type: multipart/form-data

file: [文件]

配置选项

UploadModuleOptions

参数说明类型默认值
uploadDir上传目录(相对于项目根目录)string'uploads'
baseUrl访问前缀(用于拼接文件 URL)string-
maxSize最大文件大小(字节)number10 * 1024 * 1024 (10MB)
allowedTypes允许的 MIME 类型string[]-
allowedSuffixes允许的文件后缀(不带点)string[]-
scenes场景级配置覆盖Record<string, SceneConfig>-

场景配置 (SceneConfig)

每个场景可以覆盖全局配置:

ts
scenes: {
  avatar: {
    maxSize: 2 * 1024 * 1024,        // 2MB
    allowedTypes: ['image/png', 'image/jpeg'],
    allowedSuffixes: ['png', 'jpg', 'jpeg'],
  },
  video: {
    maxSize: 500 * 1024 * 1024,       // 500MB
    allowedTypes: ['video/mp4'],
    allowedSuffixes: ['mp4'],
  },
}

完整配置示例

ts
import { Module } from '@nestjs/common';
import { UploadModule } from '@vup/nest-upload';

@Module({
  imports: [
    UploadModule.forRoot({
      uploadDir: 'uploads',
      baseUrl: 'https://your-domain.com/uploads',
      maxSize: 10 * 1024 * 1024, // 10MB
      allowedSuffixes: ['png', 'jpg', 'jpeg', 'pdf'],
      scenes: {
        avatar: {
          maxSize: 2 * 1024 * 1024,
          allowedTypes: ['image/png', 'image/jpeg'],
        },
        document: {
          maxSize: 5 * 1024 * 1024,
          allowedSuffixes: ['pdf', 'doc', 'docx'],
        },
        video: {
          maxSize: 500 * 1024 * 1024,
          allowedTypes: ['video/mp4'],
        },
      },
    }),
  ],
})
export class AppModule {}

HTTP 接口

上传文件

http
POST /upload?scene=avatar
Content-Type: multipart/form-data

file: [文件]

响应示例:

json
{
  "id": "123456",
  "filename": "avatar.jpg",
  "originalName": "my-avatar.jpg",
  "mimeType": "image/jpeg",
  "size": 102400,
  "url": "https://your-domain.com/uploads/avatar/2024-01-19/avatar.jpg",
  "scene": "avatar",
  "createdAt": "2024-01-19T10:00:00.000Z"
}

获取文件列表

http
GET /upload/list?scene=avatar&page=1&limit=10

响应示例:

json
{
  "data": [
    {
      "id": "123456",
      "filename": "avatar.jpg",
      "url": "https://your-domain.com/uploads/avatar/2024-01-19/avatar.jpg",
      "size": 102400,
      "createdAt": "2024-01-19T10:00:00.000Z"
    }
  ],
  "total": 100,
  "page": 1,
  "limit": 10
}

获取文件详情

http
GET /upload/:id

删除文件

http
DELETE /upload/:id

文件存储结构

文件按照以下结构存储:

{uploadDir}/{scene}/{YYYY-MM-DD}/{filename}

例如:

uploads/
  ├─ avatar/
  │  ├─ 2024-01-19/
  │  │  ├─ avatar-001.jpg
  │  │  └─ avatar-002.jpg
  │  └─ 2024-01-20/
  │     └─ avatar-003.jpg
  ├─ document/
  │  └─ 2024-01-19/
  │     └─ doc-001.pdf
  └─ default/
     └─ 2024-01-19/
        └─ file-001.png

当未传 scene 时使用默认场景 default

自定义 Entity

如果需要扩展表结构,可以继承 BaseUploadEntity

ts
import { BaseUploadEntity } from '@vup/nest-upload';
import { Column, Entity } from 'typeorm';

@Entity('custom_upload_files')
export class CustomUploadEntity extends BaseUploadEntity {
  @Column({ name: 'biz_type', length: 64 })
  bizType!: string;

  @Column({ name: 'biz_id', length: 64 })
  bizId!: string;

  @Column({ name: 'user_id', length: 64, nullable: true })
  userId?: string;
}

然后在 UploadModule 配置中指定:

ts
UploadModule.forRoot({
  entity: CustomUploadEntity,
  uploadDir: 'uploads',
  baseUrl: 'http://localhost:3000/uploads',
});

注意: TypeORM 需要在业务项目的 TypeOrmModule.forRoot 中注册该实体。

使用 Service

除了 HTTP 接口,也可以直接使用 UploadService

ts
import { Injectable } from '@nestjs/common';
import { UploadService } from '@vup/nest-upload';

@Injectable()
export class MyService {
  constructor(private readonly uploadService: UploadService) {}

  async uploadFile(file: Express.Multer.File, scene?: string) {
    return await this.uploadService.upload(file, scene);
  }

  async getFileList(scene?: string, page = 1, limit = 10) {
    return await this.uploadService.getList(scene, page, limit);
  }

  async deleteFile(id: string) {
    return await this.uploadService.delete(id);
  }
}

前端集成示例

使用 FormData 上传

typescript
async function uploadFile(file: File, scene?: string) {
  const formData = new FormData();
  formData.append('file', file);

  const url = scene ? `/upload?scene=${scene}` : '/upload';

  const response = await fetch(url, {
    method: 'POST',
    body: formData,
  });

  return await response.json();
}

// 使用示例
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
  const file = (e.target as HTMLInputElement).files?.[0];
  if (file) {
    const result = await uploadFile(file, 'avatar');
    console.log('上传成功:', result);
  }
});

使用 axios 上传

typescript
import axios from 'axios';

async function uploadFile(file: File, scene?: string) {
  const formData = new FormData();
  formData.append('file', file);

  const { data } = await axios.post(
    scene ? `/upload?scene=${scene}` : '/upload',
    formData,
    {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    }
  );

  return data;
}

注意事项

  • 默认使用 LocalStorageAdapter,文件存储在本地文件系统
  • 需要配置 TypeORM 才能使用数据库功能
  • 文件大小和类型限制在配置时设置,上传时会自动验证
  • 建议在生产环境中配置 baseUrl 为实际的域名
  • 文件路径中的日期格式为 YYYY-MM-DD,便于按日期管理文件
  • 如需使用云存储(OSS、S3 等),可以实现自定义 StorageAdapter

相关资源