瀏覽代碼

feat: 文件上传api

IlhamTahir 1 年之前
父節點
當前提交
0634862282

+ 8 - 0
.env.example

@@ -8,7 +8,15 @@ DATACENTER_ID=1
 APP_TITLE='API Service'
 JWT_SECRET=secret
 JWT_EXPIRES_IN=1h
+
+# Wechat
 WX_APPID=wx123456
 WX_SECRET=secret
 WX_TOKEN=token
 WX_AES_KEY=aeskey
+
+#COS 对象存储
+COS_SECRET_ID=yourid
+COS_SECRET_KEY=yourkey
+COS_REGION=ap-guangzhou
+COS_BUCKET=examplebucket-1250000000

+ 4 - 1
package.json

@@ -30,6 +30,7 @@
     "bcrypt": "^5.1.1",
     "class-transformer": "^0.5.1",
     "class-validator": "^0.14.1",
+    "cos-nodejs-sdk-v5": "^2.14.6",
     "dayjs": "^1.11.13",
     "jsonwebtoken": "^9.0.2",
     "mysql2": "^3.11.4",
@@ -37,7 +38,8 @@
     "reflect-metadata": "^0.2.0",
     "rxjs": "^7.8.1",
     "snowflake-id": "^1.1.0",
-    "typeorm": "^0.3.20"
+    "typeorm": "^0.3.20",
+    "uuid": "^11.0.3"
   },
   "devDependencies": {
     "@nestjs/cli": "^10.0.0",
@@ -46,6 +48,7 @@
     "@types/bcrypt": "^5.0.2",
     "@types/express": "^4.17.17",
     "@types/jest": "^29.5.2",
+    "@types/multer": "^1.4.12",
     "@types/node": "^20.3.1",
     "@types/supertest": "^6.0.0",
     "@typescript-eslint/eslint-plugin": "^6.0.0",

文件差異過大導致無法顯示
+ 401 - 0
pnpm-lock.yaml


+ 35 - 0
src/core/controller/file.controller.ts

@@ -0,0 +1,35 @@
+import {
+  Controller,
+  Post,
+  UploadedFile,
+  UseInterceptors,
+} from '@nestjs/common';
+import { FileInterceptor } from '@nestjs/platform-express';
+import { ApiBody, ApiConsumes, ApiOkResponse } from '@nestjs/swagger';
+import { FileService } from '../service/file.service';
+import { NoAuth } from '../decorators/no-auth.decorator';
+import { FileVo } from '../vo/file.vo';
+
+@Controller('files')
+export class FileController {
+  constructor(private readonly fileService: FileService) {}
+  @Post('upload')
+  @ApiConsumes('multipart/form-data')
+  @NoAuth()
+  @ApiBody({
+    description: '上传文件',
+    schema: {
+      type: 'object',
+      properties: {
+        file: { type: 'string', format: 'binary' },
+      },
+    },
+  })
+  @ApiOkResponse({
+    type: FileVo,
+  })
+  @UseInterceptors(FileInterceptor('file'))
+  uploadFile(@UploadedFile() file: Express.Multer.File) {
+    return this.fileService.uploadFile(file);
+  }
+}

+ 11 - 1
src/core/core.module.ts

@@ -16,9 +16,17 @@ import { Permission } from './entity/permission.entity';
 import { UserController } from './controller/user.controller';
 import { UserBindService } from './service/user-bind.service';
 import { UserBind } from './entity/user-bind.entity';
+import { FileController } from './controller/file.controller';
+import { FileService } from './service/file.service';
+import { CosStrategy } from './service/storage-strategy/cos.strategy';
 
 @Module({
-  controllers: [TokenController, RoleController, UserController],
+  controllers: [
+    TokenController,
+    RoleController,
+    UserController,
+    FileController,
+  ],
   imports: [
     ConfigModule.forRoot({
       isGlobal: true,
@@ -63,6 +71,8 @@ import { UserBind } from './entity/user-bind.entity';
     AuthService,
     RoleService,
     UserBindService,
+    FileService,
+    CosStrategy,
   ],
   exports: [UserService, UserBindService, AuthService],
 })

+ 26 - 0
src/core/service/file.service.ts

@@ -0,0 +1,26 @@
+import { FileStorage } from './storage-strategy/file-storage.interface';
+import { CosStrategy } from './storage-strategy/cos.strategy';
+import { Injectable } from '@nestjs/common';
+import { FileVo } from '../vo/file.vo';
+
+@Injectable()
+export class FileService {
+  private readonly strategies: Record<string, FileStorage>;
+
+  constructor(private readonly cosStrategy: CosStrategy) {
+    this.strategies = {
+      cos: cosStrategy,
+    };
+  }
+
+  getStrategy(type: string): FileStorage {
+    return this.strategies[type] || this.cosStrategy;
+  }
+
+  async uploadFile(
+    file: Express.Multer.File,
+    options: any = { type: 'cos' },
+  ): Promise<FileVo> {
+    return this.getStrategy(options.type).uploadFile(file, options);
+  }
+}

+ 81 - 0
src/core/service/storage-strategy/cos.strategy.ts

@@ -0,0 +1,81 @@
+import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
+import { FileStorage } from './file-storage.interface';
+import { ConfigService } from '@nestjs/config';
+import { v4 as uuidV4 } from 'uuid';
+import * as COS from 'cos-nodejs-sdk-v5';
+import { FileVo } from '../../vo/file.vo';
+
+@Injectable()
+export class CosStrategy implements FileStorage {
+  private client: COS;
+  private readonly bucket: string;
+  private readonly region: string;
+
+  constructor(private readonly configService: ConfigService) {
+    this.bucket = configService.get<string>('COS_BUCKET');
+    this.region = configService.get<string>('COS_REGION');
+    this.client = new COS({
+      SecretId: this.configService.get<string>('COS_SECRET_ID'),
+      SecretKey: this.configService.get<string>('COS_SECRET_KEY'),
+    });
+  }
+
+  async uploadFile(
+    file: Express.Multer.File,
+    options?: { folder?: string },
+  ): Promise<FileVo> {
+    const folder = options.folder || 'uploads';
+    const key = `${folder}/${uuidV4()}-${file.originalname}`;
+    const result = await new Promise<COS.PutObjectResult>((resolve, reject) => {
+      this.client.putObject(
+        {
+          Bucket: this.bucket,
+          Region: this.region,
+          Key: key,
+          Body: file.buffer,
+          ContentType: file.mimetype,
+        },
+        (err, data) => {
+          if (err) {
+            reject(err);
+          } else {
+            resolve(data);
+          }
+        },
+      );
+    });
+    if ('Location' in result) {
+      return { url: 'https://' + result.Location };
+    } else {
+      throw new HttpException(
+        'Failed to retrieve file URL from COS response',
+        HttpStatus.INTERNAL_SERVER_ERROR,
+      );
+    }
+  }
+
+  async deleteFile(filePath: string): Promise<void> {
+    const key = filePath.split('.com/')[1];
+
+    if (!key) {
+      throw new Error('Invalid filePath provided');
+    }
+
+    await new Promise<void>((resolve, reject) => {
+      this.client.deleteObject(
+        {
+          Bucket: this.bucket,
+          Region: this.region,
+          Key: key,
+        },
+        (err) => {
+          if (err) {
+            reject(err);
+          } else {
+            resolve();
+          }
+        },
+      );
+    });
+  }
+}

+ 6 - 0
src/core/service/storage-strategy/file-storage.interface.ts

@@ -0,0 +1,6 @@
+import { FileVo } from '../../vo/file.vo';
+
+export interface FileStorage {
+  uploadFile(file: Express.Multer.File, options?: any): Promise<FileVo>;
+  deleteFile(filePath: string): Promise<void>;
+}

+ 9 - 0
src/core/vo/file.vo.ts

@@ -0,0 +1,9 @@
+import { ApiProperty, ApiSchema } from '@nestjs/swagger';
+
+@ApiSchema({
+  name: 'File',
+})
+export class FileVo {
+  @ApiProperty()
+  url: string;
+}

部分文件因文件數量過多而無法顯示