Skip to content

预计时间

2 周

学习目标

  • 搭建 NestJS 项目
  • 用 Prisma 管理数据库
  • 实现 JWT 认证 + RBAC 权限
  • 文件上传到 OSS

一、为什么选 NestJS?

text
作为前端开发者,NestJS 有天然亲近感:

✅ 装饰器语法 — @Controller(), @Get(), @Post() → 类似 Angular / Spring
✅ 模块化 — Module → Controller → Service,层次清晰
✅ 依赖注入 — 开箱即用,不用自己搭
✅ TypeScript — 类型安全
✅ Swagger 自动生成 — 写完代码就有 API 文档

二、项目结构

text
src/
├── main.ts                    # 入口:启动应用
├── app.module.ts              # 根模块
├── common/                    # 公共
│   ├── guards/                # 鉴权守卫
│   │   └── auth.guard.ts
│   ├── decorators/            # 自定义装饰器
│   │   └── current-user.decorator.ts
│   └── filters/               # 异常过滤器
│       └── http-exception.filter.ts
├── auth/                      # 认证模块
│   ├── auth.module.ts
│   ├── auth.controller.ts
│   ├── auth.service.ts
│   └── dto/                   # 数据传输对象
│       ├── login.dto.ts
│       └── register.dto.ts
├── users/                     # 用户模块
│   ├── users.module.ts
│   ├── users.controller.ts
│   └── users.service.ts
├── documents/                 # 文档模块
│   ├── documents.module.ts
│   ├── documents.controller.ts
│   ├── documents.service.ts
│   └── dto/
└── prisma/
    ├── schema.prisma          # 数据库模型定义
    └── prisma.service.ts      # Prisma 客户端封装

2.1 起步

bash
npm i -g @nestjs/cli
nest new ai-saas-backend
cd ai-saas-backend

npm install @nestjs/config @prisma/client prisma
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install -D @types/passport-jwt @types/bcrypt

2.2 Prisma Schema

prisma
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Organization {
  id        String   @id @default(uuid())
  name      String
  slug      String   @unique
  plan      String   @default("free")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  users     User[]
  documents Document[]

  @@map("organizations")
}

model User {
  id             String   @id @default(uuid())
  organizationId String   @map("organization_id")
  email          String   @unique
  passwordHash   String   @map("password_hash")
  role           String   @default("member")  // admin | member
  createdAt      DateTime @default(now()) @map("created_at")

  organization   Organization @relation(fields: [organizationId], references: [id])

  @@map("users")
}

model Document {
  id             String   @id @default(uuid())
  organizationId String   @map("organization_id")
  uploadedById   String   @map("uploaded_by_id")
  title          String
  fileType       String   @map("file_type")
  fileSize       BigInt   @map("file_size")
  fileUrl        String   @map("file_url")
  status         String   @default("processing")
  createdAt      DateTime @default(now()) @map("created_at")

  organization   Organization @relation(fields: [organizationId], references: [id])
  uploadedBy     User         @relation(fields: [uploadedById], references: [id])

  @@index([organizationId, status])
  @@map("documents")
}
bash
npx prisma migrate dev --name init   # 创建迁移
npx prisma generate                   # 生成类型

2.3 Prisma Service

typescript
// src/prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnModuleInit, OnModuleDestroy
{
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

三、JWT 认证

3.1 Auth Service

typescript
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(
    private prisma: PrismaService,
    private jwtService: JwtService,
  ) {}

  async login(email: string, password: string) {
    const user = await this.prisma.user.findUnique({ where: { email } });
    if (!user) throw new UnauthorizedException('邮箱或密码错误');

    const valid = await bcrypt.compare(password, user.passwordHash);
    if (!valid) throw new UnauthorizedException('邮箱或密码错误');

    const token = this.jwtService.sign({
      sub: user.id,
      orgId: user.organizationId,
      role: user.role,
    });

    return { accessToken: token };
  }

  async register(email: string, password: string, orgName: string) {
    const passwordHash = await bcrypt.hash(password, 10);

    return this.prisma.$transaction(async (tx) => {
      const org = await tx.organization.create({
        data: { name: orgName, slug: orgName.toLowerCase().replace(/\s+/g, '-') },
      });

      const user = await tx.user.create({
        data: { email, passwordHash, organizationId: org.id, role: 'admin' },
      });

      return user;
    });
  }
}

3.2 JWT Guard

typescript
// src/common/guards/auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

// RBAC 守卫
@Injectable()
export class RolesGuard {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.includes(user.role);
  }
}

3.3 使用

typescript
// src/documents/documents.controller.ts
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../common/guards/auth.guard';
import { RolesGuard } from '../common/guards/auth.guard';
import { Roles } from '../common/decorators/roles.decorator';

@Controller('documents')
@UseGuards(JwtAuthGuard)  // 所有接口都需要登录
export class DocumentsController {
  @Get()
  findAll() {
    // 返回当前组织的文档列表
  }

  @Post()
  @UseGuards(RolesGuard)
  @Roles('admin')  // 只有 admin 能上传
  upload() { }
}

四、文件上传

typescript
// 安装依赖
// npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner multer

import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Injectable } from '@nestjs/common';

@Injectable()
export class StorageService {
  private s3 = new S3Client({
    region: process.env.S3_REGION,
    endpoint: process.env.S3_ENDPOINT,  // 兼容阿里云 OSS / MinIO
    credentials: {
      accessKeyId: process.env.S3_ACCESS_KEY!,
      secretAccessKey: process.env.S3_SECRET_KEY!,
    },
  });

  async uploadFile(file: Express.Multer.File, key: string) {
    await this.s3.send(new PutObjectCommand({
      Bucket: process.env.S3_BUCKET,
      Key: key,
      Body: file.buffer,
      ContentType: file.mimetype,
    }));

    return `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKET}/${key}`;
  }

  // 生成预签名 URL(临时下载链接,60 分钟有效)
  async getSignedUrl(key: string) {
    const command = new PutObjectCommand({
      Bucket: process.env.S3_BUCKET,
      Key: key,
    });
    return getSignedUrl(this.s3, command, { expiresIn: 3600 });
  }
}

五、多租户数据隔离

在 NestJS 中通过中间件 + Prisma 实现自动 tenant 隔离:

typescript
// 中间件自动注入 tenantId
import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class TenantMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    // JWT 中取出 orgId,注入到 request
    req.tenantId = req.user?.orgId;
    next();
  }
}

// Service 中所有查询自动带 tenantId
@Injectable()
export class DocumentsService {
  constructor(private prisma: PrismaService) {}

  async findAll(tenantId: string) {
    return this.prisma.document.findMany({
      where: { organizationId: tenantId },  // ← 强制过滤
    });
  }
}

推荐资源


实践

用 NestJS 搭一个简单的知识库 API:

  1. nest new knowledge-api
  2. 配置 Prisma + PostgreSQL
  3. 实现注册/登录(JWT)
  4. 实现文档 CRUD
  5. 加上 Swagger 文档:npm install @nestjs/swaggerSwaggerModule.setup('api', app, document)