import { Injectable, NestMiddleware, BadRequestException, } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import multer, { Multer } from 'multer'; import path from 'path'; import fs from 'fs'; const ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/webp']; const MAX_SIZE = 2 * 1024 * 1024; // 2M /** * 文件上传中间件(按日期分文件夹) * 单图字段名固定:file */ @Injectable() export class UploadMiddleware implements NestMiddleware { private readonly upload: Multer; constructor() { const root = process.env.UPLOAD_ROOT ?? './uploads'; this.upload = multer({ storage: multer.diskStorage({ destination: (_req, _file, cb) => { const date = new Date(); const dir = path.join( process.cwd(), root.replace(/^\.\/?/, ''), `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}`, ); fs.mkdirSync(dir, { recursive: true }); cb(null, dir); }, filename: (_req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase(); const name = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`; cb(null, name); }, }), fileFilter: (_req, file, cb) => { if (ALLOWED_MIME.includes(file.mimetype)) { cb(null, true); } else { // FileFilterCallback 重载:错误时只传 Error,不传 acceptFile cb(new Error('仅支持 jpg/png/jpeg/webp 格式图片')); } }, limits: { fileSize: MAX_SIZE }, }); } use(req: Request, _res: Response, next: NextFunction): void { // @types/express 与 @types/multer 内嵌的 @types/express-serve-static-core 版本不一致, // 运行时完全等价,这里用一次类型断言绕过编译期差异 const single = this.upload.single('file') as ( req: Request, res: Response, cb: (err: unknown) => void, ) => void; single(req, _res, (err) => { if (err) { next(new BadRequestException(err.message ?? '文件上传失败')); return; } next(); }); } }