Chuyển đến nội dung chính

第 16 課:文件上傳、快取和任務調度

使用 Multer、S3 儲存上傳檔案。使用 Redis 進行快取。使用@nestjs/schedule 進行任務調度。與公牛一起排隊。

💻 程式設計 — 第 16 課 第 16 課:文件上傳、快取和任務 調度

NestJS:從基礎到高級

第 4 部分:進階功能

亞洲開發網

1. 使用Multer上傳文件

npm install @nestjs/platform-express multer
npm install -D @types/multer
// files/files.controller.ts
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';

@Controller('files')
export class FilesController {
  // Upload 1 file
  @Post('upload')
  @UseInterceptors(FileInterceptor('file', {
    storage: diskStorage({
      destination: './uploads',
      filename: (req, file, cb) => {
        const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
        cb(null, `${uniqueName}${extname(file.originalname)}`);
      },
    }),
    limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
    fileFilter: (req, file, cb) => {
      const allowed = /\.(jpg|jpeg|png|gif|webp)$/i;
      if (!allowed.test(extname(file.originalname))) {
        return cb(new BadRequestException('Only image files allowed'), false);
      }
      cb(null, true);
    },
  }))
  uploadFile(@UploadedFile() file: Express.Multer.File) {
    return {
      filename: file.filename,
      size: file.size,
      url: `/uploads/${file.filename}`,
    };
  }

  // Upload nhiều files
  @Post('upload-multiple')
  @UseInterceptors(FilesInterceptor('files', 10))
  uploadMultiple(@UploadedFiles() files: Express.Multer.File[]) {
    return files.map((f) => ({
      filename: f.filename,
      size: f.size,
    }));
  }
}

上傳到S3

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

@Injectable()
export class S3Service {
  private s3: S3Client;

  constructor(private config: ConfigService) {
    this.s3 = new S3Client({
      region: config.get('AWS_REGION'),
      credentials: {
        accessKeyId: config.get('AWS_ACCESS_KEY_ID'),
        secretAccessKey: config.get('AWS_SECRET_ACCESS_KEY'),
      },
    });
  }

  async upload(file: Express.Multer.File, folder: string): Promise<string> {
    const key = `${folder}/${Date.now()}-${file.originalname}`;
    
    await this.s3.send(new PutObjectCommand({
      Bucket: this.config.get('AWS_S3_BUCKET'),
      Key: key,
      Body: file.buffer,
      ContentType: file.mimetype,
    }));

    return `https://${this.config.get('AWS_S3_BUCKET')}.s3.amazonaws.com/${key}`;
  }
}

2.Redis緩存

npm install @nestjs/cache-manager cache-manager cache-manager-redis-yet
// app.module.ts
import { CacheModule } from '@nestjs/cache-manager';
import { redisStore } from 'cache-manager-redis-yet';

@Module({
  imports: [
    CacheModule.registerAsync({
      isGlobal: true,
      useFactory: async () => ({
        store: await redisStore({
          socket: { host: 'localhost', port: 6379 },
          ttl: 60_000, // 60 giây default
        }),
      }),
    }),
  ],
})
export class AppModule {}
// Sử dụng trong service
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';

@Injectable()
export class ProductsService {
  constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}

  async findAll(): Promise<Product[]> {
    const cacheKey = 'products:all';
    const cached = await this.cache.get<Product[]>(cacheKey);
    if (cached) return cached;

    const products = await this.productRepo.find();
    await this.cache.set(cacheKey, products, 300_000); // 5 phút
    return products;
  }

  async update(id: string, dto: UpdateProductDto) {
    const product = await this.productRepo.save({ id, ...dto });
    // Invalidate cache
    await this.cache.del('products:all');
    await this.cache.del(`products:${id}`);
    return product;
  }
}

快取裝飾器

// Custom cache decorator
export function Cacheable(key: string, ttl = 60_000) {
  return applyDecorators(
    UseInterceptors(CacheInterceptor),
    SetMetadata('cache_key', key),
    SetMetadata('cache_ttl', ttl),
  );
}

// Sử dụng
@Get()
@Cacheable('products:all', 300_000)
findAll() {
  return this.productsService.findAll();
}

3. 任務調度

npm install @nestjs/schedule
// app.module.ts
import { ScheduleModule } from '@nestjs/schedule';

@Module({
  imports: [ScheduleModule.forRoot()],
})
export class AppModule {}
// tasks/tasks.service.ts
import { Cron, CronExpression, Interval, Timeout } from '@nestjs/schedule';

@Injectable()
export class TasksService {
  private readonly logger = new Logger(TasksService.name);

  // Chạy mỗi ngày lúc 2:00 AM
  @Cron('0 2 * * *')
  async handleDailyCleanup() {
    this.logger.log('Running daily cleanup...');
    await this.cleanupExpiredSessions();
    await this.cleanupOldLogs();
  }

  // Chạy mỗi 30 giây
  @Cron(CronExpression.EVERY_30_SECONDS)
  handleHealthCheck() {
    this.logger.debug('Health check ping');
  }

  // Chạy mỗi 5 phút
  @Interval(5 * 60 * 1000)
  async syncExternalData() {
    this.logger.log('Syncing external data...');
  }

  // Chạy 1 lần sau 10 giây khi app start
  @Timeout(10_000)
  async onceAfterStartup() {
    this.logger.log('Warming up caches...');
    await this.warmupCache();
  }
}

4. 與 Bull 一起排隊

npm install @nestjs/bull bull
npm install -D @types/bull
// app.module.ts
import { BullModule } from '@nestjs/bull';

@Module({
  imports: [
    BullModule.forRoot({
      redis: { host: 'localhost', port: 6379 },
    }),
    BullModule.registerQueue({ name: 'email' }),
    BullModule.registerQueue({ name: 'image-processing' }),
  ],
})
export class AppModule {}
// email/email.processor.ts
import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bull';

@Processor('email')
export class EmailProcessor {
  @Process('send-welcome')
  async handleWelcomeEmail(job: Job<{ email: string; name: string }>) {
    const { email, name } = job.data;
    await this.mailerService.send({
      to: email,
      subject: `Chào mừng ${name}!`,
      template: 'welcome',
      context: { name },
    });
  }

  @Process('send-reset-password')
  async handleResetPassword(job: Job<{ email: string; token: string }>) {
    // Gửi email reset password
  }
}
// Thêm job vào queue
@Injectable()
export class UsersService {
  constructor(@InjectQueue('email') private emailQueue: Queue) {}

  async register(dto: CreateUserDto) {
    const user = await this.userRepo.save(dto);
    
    await this.emailQueue.add('send-welcome', {
      email: user.email,
      name: user.name,
    }, {
      delay: 5000,        // Delay 5 giây
      attempts: 3,        // Retry 3 lần nếu fail
      backoff: { type: 'exponential', delay: 2000 },
    });

    return user;
  }
}

5. 總結

  • 文件上傳:Multer 用於本地,S3 用於生產,驗證文件類型+大小
  • 快取:Redis 支援的快取、快取失效、自訂裝飾器
  • 調度:後台任務的 Cron 作業、間隔、逾時
  • 佇列:Bull + Redis 用於非同步作業處理(電子郵件、圖像、報告)

下一篇文章將探討 NestJS 微服務。