🚀 NestJS + Prisma + BullMQ

现代 Node.js 后端开发三剑客 · 完整教程

📖 适合读者:会 TypeScript + 基础 Node.js,想快速掌握这三个技术栈

为什么需要这三个技术?

🔴 传统 Node.js 后端的问题

假设你用 Express 开发一个用户系统:

 1// app.js - 所有代码混在一起
 2const express = require('express');
 3const app = express();
 4 5// 用户相关代码...
 6// 订单相关代码...
 7// 邮件相关代码...
 8// 数据库相关代码...
 9// ...几百行后...

问题:代码一多就变成"意大利面条",没人敢改。

🏗️ NestJS 解决什么问题?

像厨房一样分工明确:服务员接单、厨师做菜、传菜员上菜。NestJS 把代码组织成模块(Module),每个模块管自己的控制器(接单)、服务(做菜)、数据访问(拿食材)。

🗄️ Prisma 解决什么问题?

操作数据库要写 SQL,太麻烦而且容易出错。Prisma 让你用 JavaScript 对象的方式操作数据库,自动生成 SQL,还能做类型检查。

⚡ BullMQ 解决什么问题?

发邮件、AI 审查这种耗时操作,不能让用户干等。BullMQ 把任务放进队列,后台慢慢处理,用户立刻得到响应。

传统方式(同步)

用户提交订单 → 系统处理(等5秒)→ 返回成功

用户等待,体验差

队列方式(异步)

用户提交订单 → 立即返回 → 后台处理

用户不等待,体验好

🏗️
NestJS 入门
基于 TypeScript 的后端框架

一句话理解

NestJS 是 Node.js 的后端框架,它借鉴了 Angular 的设计思想,用装饰器(@Controller、@Injectable)来声明代码的角色,让代码结构清晰、可测试、可维护。

核心概念

概念做什么类比
@Controller接收 HTTP 请求餐厅服务员
@Injectable处理业务逻辑厨师
@Module打包相关功能厨房分区
@Pipe数据验证/转换质检员

第一个 Controller

// user.controller.ts

// @Controller('users') - 我是 users 模块的控制器
// 访问路径前缀是 /users
@Controller('users')
export class UsersController {

  // @Get() - 处理 GET 请求
  // 完整路径: GET /users
  @Get()
  findAll() {
    return [
      { id: 1, name: '张三' },
      { id: 2, name: '李四' },
    ];
  }

  // @Get(':id') - :id 是路由参数
  // 完整路径: GET /users/123
  @Get(':id')
  findOne(@Param('id') id: string) {
    return { id, name: '用户' + id };
  }
}

第一个 Service

💡 类比
Controller 是服务员,只负责接收订单和端菜;Service 是厨师,负责真正做菜。
// user.service.ts

// @Injectable() - 表示此类可以被依赖注入
// NestJS 会自动创建这个类的实例
@Injectable()
export class UsersService {

  // 模拟数据库中的用户
  private users = [
    { id: 1, name: '张三', email: 'zhangsan@example.com' },
    { id: 2, name: '李四', email: 'lisi@example.com' },
  ];

  findAll() {
    return this.users;
  }

  findById(id: number) {
    return this.users.find(u => u.id === id);
  }

  create(data: { name: string; email: string }) {
    const newUser = { id: this.users.length + 1, ...data };
    this.users.push(newUser);
    return newUser;
  }
}

依赖注入是什么?

💉 依赖注入(DI)

简单说:你不用 `new UserService()`,而是告诉 NestJS "我需要一个 UserService",它自动给你。

// 不用 DI(手动创建)
const service = new UserService();

// 用 DI(NestJS 自动注入)
@Controller('users')
export class UsersController {
  // 在 constructor 中声明依赖
  constructor(private readonly usersService: UsersService) {}
  
  findAll() {
    // 直接用,NestJS 已经注入好了
    return this.usersService.findAll();
  }
}

NestJS 进阶

模块化架构

一个完整的 NestJS 应用由多个 Module 组成,每个 Module 包含自己的 Controller、Service、Repository。

// users.module.ts
@Module({
  controllers: [UsersController],   // 控制器
  providers: [UsersService],       // 服务(可注入的)
  exports: [UsersService],         // 导出给其他模块用
})
export class UsersModule {}

// app.module.ts - 根模块
@Module({
  imports: [UsersModule],        // 引入子模块
})
export class AppModule {}

中间件与拦截器

Middleware(中间件)

请求到达 Controller 之前执行

用途:日志、鉴权、CORS

Interceptor(拦截器)

响应返回之前执行

用途:统一包装响应、缓存

实战:完整 CRUD 模块

// posts.controller.ts - 完整的 CRUD 控制器
@Controller('posts')
export class PostsController {
  
  @Post()           // POST /posts - 创建
  create(@Body() dto: CreatePostDto) {
    return this.postsService.create(dto);
  }

  @Get()           // GET /posts - 列表
  findAll() {
    return this.postsService.findAll();
  }

  @Get(':id')       // GET /posts/:id - 单个
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.postsService.findOne(id);
  }

  @Patch(':id')      // PATCH /posts/:id - 部分更新
  update(@Param('id') id: number, @Body() dto: UpdatePostDto) {
    return this.postsService.update(id, dto);
  }

  @Delete(':id')     // DELETE /posts/:id - 删除
  remove(@Param('id') id: number) {
    return this.postsService.remove(id);
  }
}
🗄️
Prisma 入门
现代化的 TypeScript ORM

一句话理解

Prisma 是一个数据库工具,它让你用声明式的方式定义数据结构,然后用 TypeScript 语法操作数据库,不用写 SQL。

💡 类比
如果把数据库比作仓库,Prisma 就是仓库管理员。你说"给我拿这批货",Prisma 帮你找、帮你取、帮你整理好。

核心概念

概念作用
schema.prisma定义数据模型(表结构)
PrismaClient数据库客户端,帮你执行操作
npx prisma migrate把 schema 同步到数据库
npx prisma generate生成类型安全的客户端代码

定义数据模型

// prisma/schema.prisma

// generator - 指定生成什么客户端
generator client {
  provider = "prisma-client-js"
}

// datasource - 指定用什么数据库
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// model User - 定义 User 表
// @id = 主键,@default(autoincrement()) = 自动增长
// String + ? = 可选字段
model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String  @unique  // 唯一索引,不能重复
  age       Int??              // ? = 可选字段
  createdAt DateTime @default(now())
  
  // relation - 一对多关系
  // 一个 User 可以有多篇 Post
  posts     Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String??
  published Boolean @default(false)
  
  // 外键关联到 User
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
}

CRUD 操作

// ===== 创建数据 =====

// 创建单个
const user = await prisma.user.create({
  data: {
    name:  '张三',
    email: 'zhangsan@example.com',
    age:   25
  }
});

// 批量创建
const users = await prisma.user.createMany({
  data: [
    { name: '李四', email: 'lisi@example.com' },
    { name: '王五', email: 'wangwu@example.com' }
  ]
});
// ===== 读取数据 =====

// 查所有
const users = await prisma.user.findMany();

// 查单个(按唯一字段)
const user = await prisma.user.findUnique({
  where: { email: 'zhangsan@example.com' }
});

// 条件查询
const adults = await prisma.user.findMany({
  where: { age: { gt: 18 } }   // gt = greater than
});

// 分页
const page = await prisma.user.findMany({
  take: 10,    // 取 10 条
  skip: 20     // 跳过前 20 条
});
// ===== 更新数据 =====

// 更新单个
await prisma.user.update({
  where: { id: 1 },
  data: { name: '新名字' }
});

// 条件批量更新
await prisma.user.updateMany({
  where: { age: { lt: 18 } },   // lt = less than
  data: { status: 'minor' }
});
// ===== 删除数据 =====

// 删除单个
await prisma.user.delete({ where: { id: 1 } });

// 批量删除
await prisma.user.deleteMany({ where: { status: 'inactive' } });

Prisma 进阶

关联查询(Include)

// 查询用户的同时,把关联的文章也查出来
const user = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: true  // 把这个用户的文章也一起查出来
  }
});

// user.posts 就是这个用户的所有文章
console.log(user.posts);

事务(Transaction)

保证多个数据库操作要么全部成功,要么全部失败。

// 同时创建用户和用户的文章,要么全成功,要么全失败
const result = await prisma.$transaction([
  prisma.user.create({ data: { name: '张三', email: 'zhangsan@example.com' } }),
  prisma.post.create({ data: { title: '你好', authorId: 1 } })
]);

Raw SQL(原生查询)

当 Prisma 的查询构建器不够用时,可以写原生 SQL。

// 执行原生 SQL
const result = await prisma.$queryRaw<User>`
  SELECT * FROM users WHERE age > ${minAge}
`;
BullMQ 入门
基于 Redis 的任务队列

一句话理解

BullMQ 是任务队列,让你能异步执行耗时操作。用户提交任务后立即返回,后台慢慢处理,不阻塞主线程。

💡 类比
就像餐厅点餐:你点完菜,服务员说"稍等",你去喝茶聊天。厨房做好了会叫你。BullMQ 就是这个服务员和厨房的组合。

核心概念

概念类比做什么
Queue餐厅存放任务的地方
Job订单具体要执行的任务
Producer顾客把任务放进队列
Worker厨师从队列取任务执行

三步快速上手

第一步:创建队列
// queue.ts
import { Queue } from 'bullmq';

// 创建 'email' 队列,连接 Redis
const emailQueue = new Queue('email', {
  connection: { host: 'localhost', port: 6379 }
});

export default emailQueue;
第二步:添加任务(生产者)
// user.service.ts
import emailQueue from './queue';

@Injectable()
export class UserService {
  
  async register(data: { email: string; name: string }) {
    // 1. 保存用户(同步操作)
    const user = await this.prisma.user.create({ data });
    
    // 2. 把发邮件任务加入队列(立即返回,不等待)
    await emailQueue.add('welcome', {
      to: user.email,
      name: user.name
    });
    
    return user;  // 立即返回给用户
  }
}
第三步:处理任务(消费者)
// email.processor.ts
import { Worker, Job } from 'bullmq';

// 创建 Worker,自动从队列取任务执行
const worker = new Worker('email', async (job: Job) => {
  
  // job.data 是 add() 时传的数据
  const { to, name } = job.data;
  
  // 模拟发邮件
  console.log(`发送邮件给 ${to},称呼:${name}`);
  
  // 更新进度(0-100)
  await job.updateProgress(100);
  
  return { success: true };
  
}, {
  connection: { host: 'localhost', port: 6379 }
});

console.log('📧 邮件 Worker 已启动,等待任务...');

流程图

📝 add() 添加任务
📬 Redis 队列
👨‍🍳 Worker 取任务
📧 执行任务

BullMQ 进阶

延时任务

任务不立即执行,而是等一段时间后再处理。

// 5 分钟后再执行(适合:延时提醒、定时任务)
await emailQueue.add('reminder', {
  to: 'user@example.com',
  message: '该打卡了!'
}, {
  delay: 5 * 60 * 1000  // 5 分钟(毫秒)
});

重试机制

任务失败时自动重试,不用担心丢任务。

const worker = new Worker('api', async (job) => {
  
  // 模拟可能失败的操作
  if (Math.random() > 0.7) {
    throw new Error('网络错误');
  }
  
  return { success: true };
  
}, {
  connection: { host: 'localhost', port: 6379 },
  
  // 重试配置
  limiter: {
    max: 3,        // 最多重试 3 次
    duration: 5000   // 每次间隔 5 秒
  }
});

优先级队列

// 添加任务时指定优先级(数字越小优先级越高)
await emailQueue.add('vip-notify', { userId: 1 }, { priority: 1 }); // 高优先
await emailQueue.add('daily-digest', { userId: 2 }, { priority: 5 }); // 普通

重复任务(定时)

// 每小时执行一次
await emailQueue.add(
  'report',
  { type: 'daily' },
  {
    repeat: {
      every: 60 * 60 * 1000  // 每小时(毫秒)
    }
  }
);

三者如何配合使用?

📝 提交代码
🏗️ NestJS
🗄️ Prisma
⚡ 队列
🤖 AI 审查

一个完整请求的处理流程

// 1. Controller 接收请求
@Post()
async submit(@Body() dto: { code: string; language: string }) {
  
  // 2. Service 保存到数据库(Prisma)
  const task = await this.prisma.reviewTask.create({
    data: { code: dto.code, language: dto.language }
  });
  
  // 3. 把任务加入队列(BullMQ),立即返回
  await this.reviewQueue.add('review', { taskId: task.id });
  
  // 用户不用等待 AI 审查,立即返回
  return { taskId: task.id, status: 'queued' };
}

快速回顾

技术解决的问题核心 API
NestJS代码组织、模块化、依赖注入@Controller, @Injectable, @Module
Prisma数据库操作、类型安全prisma.user.create(), findMany()
BullMQ异步任务、后台处理queue.add(), new Worker()
下一步学习建议
  • 搭建一个完整的 NestJS 项目,尝试实现 CRUD
  • 用 Prisma 连接真实数据库,做一次完整的数据操作
  • 写一个 Worker,处理异步任务
  • 组合三者,做一个小应用(如任务管理、博客系统)