nest(5)-文件上传和静态资源

文件上传

文件预览

解释

🧪 测试矩阵

# 测试 结果
GET / express.static 服务 index.html(不带 token ✅ 200 公开访问
POST /user/me/avatar(带 token) 上传头像 {url: "/uploads/avatars/1-xxx.png"}
GET /uploads/avatars/xxx.png不带 token 公开访问图片 ✅ 200 image/png 70 bytes
GET /user/1(带 token) 用户 avatar 字段已更新 "avatar": "/uploads/avatars/..."
POST /user/me/avatar不带 token 上传端点鉴权 ✅ 401 "请先登录"

📁 项目结构更新

vbnet 复制代码
public/                          ← 🆕 静态资源根目录(express.static 映射)
├── index.html                   ← 🆕 验证用
└── uploads/
    └── avatars/
        └── 1-1788943175071.png  ← 上传自动生成 {userId}-{timestamp}.{ext}
                                    每次上传自动重命名 + 删除旧头像

src/
├── user/
│   ├── entities/user.entity.ts  ← 🆕 加了 avatar: string(nullable)
│   ├── user.service.ts          ← 🆕 uploadAvatar() 方法
│   └── user.controller.ts       ← 🆕 POST /user/me/avatar 端点
├── common/
│   └── middleware/              ← 之前创建的 requestId + cors
└── main.ts                      ← 🆕 express.static(path.join(cwd, 'public'))

🔑 回答你的核心问题:静态资源要不要权限?

arduino 复制代码
┌──────────────────────────────────────────────────────────────────┐
│  express.static 的本质:Middleware 层                            │
│                                                                  │
│  请求 → requestId → cors → express.static → JwtAuthGuard        │
│                         ↑                      ↑                 │
│                         │                      │                 │
│                    如果文件匹配            如果文件不匹配          │
│                    res.sendFile()         继续往下走             │
│                    (不进 NestJS)           → 进 Guard → Controller│
└──────────────────────────────────────────────────────────────────┘

两种策略对应两种资源类型:

资源类型 例子 策略 实现方式
公开资源 头像、帖子图片、商品图 express.static 直接暴露 上传 → public/uploads/xxx.png任何人都能 GET
私有资源 合同、病历、私密附件 不走 express.static NestJS Controller + @UseGuards + res.sendFile()

🛡️ 私有文件怎么保护?(了解即可,需要时再做)

ts 复制代码
// 私有文件的正确姿势:走 NestJS Controller,不用 express.static
@Controller('private')
@UseGuards(JwtAuthGuard)  // ← NestJS Guard 能拦!
export class PrivateFileController {
  
  @Get(':filename')
  download(@Param('filename') filename: string, @CurrentUser() user: any, res: Response) {
    // ① 数据权限检查:这个文件属于当前用户吗?
    const file = await this.fileRepo.findOne({ where: { filename, ownerId: user.sub } });
    if (!file) throw new ForbiddenException('无权访问');
    
    // ② 从 private/ 目录读(注意 private 不要放在 public 下面!)
    const filePath = path.join(process.cwd(), 'private', filename);
    res.sendFile(filePath);
  }
}

🎯 你的 NestJS 完整 AOP 体系(现在多了文件上传 + 静态资源)

scss 复制代码
请求进来
   │
   ▼
┌────────────────────────────────────────────────┐
│  ⬇️ Middleware 层(Express 原生)               │
│    ① requestIdMiddleware  ← 注入 trace id       │
│    ② corsMiddleware       ← 跨域 + 预检短路     │
│    ③ express.static('./public')                  │
│       ├── 匹配到文件 → res.sendFile() 返回       │
│       │              ❌ 不走 NestJS!            │
│       └── 没匹配 → next() 继续往下               │
└──────────────────────┬─────────────────────────┘
                       ▼
┌────────────────────────────────────────────────┐
│  ⬇️ Guard 层(NestJS AOP)                       │
│    JwtAuthGuard → RolesGuard                     │
│    功能权限:能调接口吗?                         │
└──────────────────────┬─────────────────────────┘
                       ▼
┌────────────────────────────────────────────────┐
│  ⬇️ Controller + Service                        │
│    multipart → FileInterceptor → multer          │
│    数据权限:能看/改哪些数据?                    │
│    文件处理:重命名 + 删除旧头像                   │
└──────────────────────┬─────────────────────────┘
                       ▼
┌────────────────────────────────────────────────┐
│  ⬇️ Interceptor → Filter                       │
│    LoggingInterceptor(带 [req=xxx] trace)      │
│    TransformInterceptor({code,msg,data})        │
│    AllExceptionsFilter(统一异常格式)            │
└────────────────────────────────────────────────┘

💡 前端调头像上传的代码长这样

js 复制代码
// 头像上传
const fileInput = document.querySelector('input[type=file]');
const fd = new FormData();
fd.append('file', fileInput.files[0]); // field 名必须是 'file'

const res = await fetch('/user/me/avatar', {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}` },
  body: fd, // Content-Type 会自动设成 multipart/form-data
});
const data = await res.json();
// data.url = "/uploads/avatars/1-1788943175071.png"

// 显示头像(直接拼接 origin + 路径,不需要 token!)
img.src = `${window.location.origin}${data.url}`;
相关推荐
遨翔在知识的海洋里1 小时前
nest(3)-jwt和RBAC
后端
遨翔在知识的海洋里1 小时前
nest(5)-Middleware
后端
程序员梅雨1 小时前
Linux & Shell 实用干货
linux·运维·服务器·后端·面试·php
大牧师2 小时前
TypeORM 入门教程
后端·sql·mysql·orm·nest·typeorm
IT_陈寒2 小时前
Vite静态资源路径这个大坑害我调了一下午
前端·人工智能·后端
oliver_sys_log2 小时前
绕过 Yearning 查询体验限制:我做了一个 DataGrip 只读 SQL 代理
后端·mysql
水深火乐2 小时前
golang-jwt v5 入门
后端
carson9552 小时前
基于springboot和vue的文本文件上传下载在线编辑功能
后端
n8n2 小时前
Spring AI 提示词工程进阶:System / User / Assistant 角色、Prompt Template 动态拼装与多角色人设切换
后端