NestJS分页查询与数据校验实战
学习目标:掌握分页查询通用参数定义、Transform 数据转换、Query 参数使用、Order 排序参数处理、自定义校验规则。
一、分页查询通用参数概述
1.1 为什么需要分页查询
code
分页查询的重要性:
│
├── 性能优化
│ ├── 减少数据库查询压力
│ ├── 减少网络传输数据量
│ └── 提升前端渲染性能
│
├── 用户体验
│ ├── 数据分批加载
│ ├── 支持上拉加载更多
│ └── 快速定位数据
│
└── 系统稳定性
├── 避免内存溢出
├── 控制数据库资源占用
└── 防止慢查询1.2 分页查询通用参数定义
typescript
// src/common/dto/pagination.dto.ts
import { IsNumber, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
import { OrderType } from './order-type.dto';
/**
* 分页查询通用参数
*/
export class PaginationDto {
/**
* 页码(从 1 开始)
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
page: number = 1;
/**
* 每页数量
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
size: number = 10;
/**
* 排序规则
* 格式:{ [fieldName]: 'asc' | 'desc' }
* 示例:{ id: 'asc', createdAt: 'desc' }
*/
@IsOptional()
order?: OrderType;
}1.3 OrderType 类型定义
typescript
// src/common/dto/order-type.dto.ts
import 'reflect-metadata';
/**
* 排序类型
* key: 字段名
* value: 排序方式(asc 升序 / desc 降序)
*/
export class OrderType {
[key: string]: 'asc' | 'desc';
}1.4 分页参数说明表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| page | number | 1 | 页码,从 1 开始 |
| size | number | 10 | 每页数量 |
| order | OrderType | undefined | 排序规则,格式:{ field: 'asc' } |
二、Service 层实现分页查询
2.1 Service 方法实现
typescript
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
/**
* 根据分类查询课程(带分页和排序)
*/
async getCoursesByType(dto: GetCoursesByTypeDto) {
// 1. 计算分页参数
const skip = dto.page ? (dto.page - 1) * (dto.size || 10) : 0;
const take = dto.size || 10;
// 2. 处理排序参数
const orderBy = dto.order ? [dto.order] : [{ order: 'asc' as const }];
// 3. 执行查询
return this.prisma.courseTypes.findMany({
skip,
take,
orderBy,
include: {
tags: {
include: {
courses: {
include: {
course: {
include: {
author: true,
},
},
},
},
},
},
},
});
}
}2.2 分页参数计算详解
code
分页参数计算逻辑:
│
├── skip(跳过多少条)
│ ├── 公式:(page - 1) * size
│ ├── page = 1, size = 10 → skip = 0
│ ├── page = 2, size = 10 → skip = 10
│ └── page = 3, size = 10 → skip = 20
│
├── take(取多少条)
│ └── 直接使用 size
│
└── orderBy(排序)
├── 格式:[{ field: 'asc' }]
├── 单字段:[{ id: 'asc' }]
└── 多字段:[{ id: 'asc' }, { createdAt: 'desc' }]2.3 排序参数处理详解
typescript
// 排序参数处理的三种情况
// 情况一:使用默认排序
const orderBy = [{ order: 'asc' as const }];
// 情况二:使用前端传递的排序
const orderBy = [dto.order]; // dto.order = { id: 'asc' }
// 情况三:多字段排序
const orderBy = [
{ id: 'asc' },
{ createdAt: 'desc' }
];三、DTO 定义与继承
3.1 创建 GetCoursesByTypeDto
typescript
// src/modules/course/dto/get-courses-by-type.dto.ts
import { PaginationDto } from '@/common/dto/pagination.dto';
/**
* 根据分类查询课程 DTO
* 继承分页通用参数
*/
export class GetCoursesByTypeDto extends PaginationDto {
// 可以添加其他特定参数
// 例如:typeId?: number;
}3.2 DTO 继承关系图
code
DTO 继承关系:
│
├── PaginationDto(通用分页参数)
│ ├── page: number
│ ├── size: number
│ └── order: OrderType
│
├── GetCoursesByTypeDto(继承)
│ └── 继承所有分页参数
│
└── 其他 DTO 示例
├── GetUsersDto extends PaginationDto
├── GetPostsDto extends PaginationDto
└── GetOrdersDto extends PaginationDto3.3 DTO 设计最佳实践
typescript
// 推荐:通用 DTO 放在 common 文件夹
// src/common/dto/pagination.dto.ts
// 推荐:业务 DTO 放在模块文件夹
// src/modules/course/dto/get-courses-by-type.dto.ts
// 推荐:使用继承复用通用参数
export class GetCoursesByTypeDto extends PaginationDto {
@IsNumber()
@IsOptional()
typeId?: number;
}四、Controller 层实现
4.1 Controller 完整实现
typescript
// src/modules/course/course.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { CourseService } from './course.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';
@Controller('courses')
export class CourseController {
constructor(private readonly courseService: CourseService) {}
/**
* 根据分类查询课程(带分页和排序)
* GET /courses/by-type?page=1&size=10&order[id]=asc
*/
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
return this.courseService.getCoursesByType(dto);
}
}4.2 Query 参数说明
code
Query 参数使用说明:
│
├── 基本参数
│ ├── ?page=1
│ ├── ?size=10
│ └── ?page=2&size=20
│
├── 排序参数(单字段)
│ └── ?order[id]=asc
│
├── 排序参数(多字段)
│ ├── ?order[id]=asc&order[createdAt]=desc
│ └── 使用 qs 库解析
│
└── 完整示例
└── ?page=1&size=10&order[id]=asc五、Transform 数据转换详解
5.1 为什么需要 Transform
code
Query 参数类型问题:
│
├── 问题
│ ├── Query 参数默认都是 string 类型
│ ├── DTO 定义的是 number 类型
│ └── 类型不匹配导致校验失败
│
├── 解决方案
│ ├── 方案一:手动转换类型
│ ├── 方案二:使用 @Type 装饰器
│ └── 方案三:全局启用 transform
│
└── 推荐方案
└── 全局启用 transform + @Type 装饰器5.2 全局启用 Transform
typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局启用 ValidationPipe 并开启 transform
app.useGlobalPipes(
new ValidationPipe({
transform: true, // 启用数据转换
whitelist: true, // 过滤掉未在 DTO 中定义的属性
}),
);
await app.listen(3000);
}
bootstrap();5.3 使用 @Type 装饰器
typescript
// src/common/dto/pagination.dto.ts
import { IsNumber, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
export class PaginationDto {
@IsNumber()
@IsOptional()
@Type(() => Number) // 自动转换为 Number 类型
page: number = 1;
@IsNumber()
@IsOptional()
@Type(() => Number) // 自动转换为 Number 类型
size: number = 10;
}5.4 Transform 工作流程
code
Transform 工作流程:
│
├── 第一步:前端发送请求
│ └── GET /courses?page=1&size=10
│ Query 参数都是 string 类型
│
├── 第二步:ValidationPipe 接收参数
│ └── transform: true 已启用
│
├── 第三步:@Type 装饰器转换类型
│ ├── @Type(() => Number) 将 '1' 转换为 1
│ └── @Type(() => Number) 将 '10' 转换为 10
│
├── 第四步:@IsNumber 校验类型
│ └── 校验转换后的类型是否为 number
│
└── 第五步:Controller 接收 DTO
└── dto.page 和 dto.size 都是 number 类型六、qs 库的使用
6.1 qs 库介绍
code
qs 库介绍:
│
├── 安装
│ ├── npm install qs
│ └── yarn add qs
│
├── 作用
│ ├── 对象转字符串:qs.stringify(obj)
│ └── 字符串转对象:qs.parse(str)
│
└── 使用场景
├── 解析复杂 Query 参数
├── 解析嵌套对象参数
└── 解析数组参数6.2 qs 库基本使用
typescript
import qs from 'qs';
// 示例一:对象转字符串
const obj = { page: 1, size: 10 };
const str = qs.stringify(obj);
console.log(str); // 'page=1&size=10'
// 示例二:字符串转对象
const str = 'page=1&size=10';
const obj = qs.parse(str);
console.log(obj); // { page: '1', size: '10' }
// 示例三:嵌套对象
const obj = { order: { id: 'asc' } };
const str = qs.stringify(obj);
console.log(str); // 'order[id]=asc'
// 示例四:解析嵌套对象
const str = 'order[id]=asc&order[createdAt]=desc';
const obj = qs.parse(str);
console.log(obj);
// { order: { id: 'asc', createdAt: 'desc' } }6.3 Postman 中使用 qs 格式参数
code
Postman 设置 Query 参数:
│
├── 单字段排序
│ ├── Key: order[id]
│ └── Value: asc
│
├── 多字段排序
│ ├── Key: order[id]
│ ├── Value: asc
│ ├── Key: order[createdAt]
│ └── Value: desc
│
├── 最终 URL
│ └── ?page=1&size=10&order[id]=asc&order[createdAt]=desc
│
└── NestJS 自动解析为
└── { page: 1, size: 10, order: { id: 'asc', createdAt: 'desc' } }七、orderBy 数组形式详解
7.1 orderBy 数组形式说明
code
Prisma orderBy 格式:
│
├── 单字段排序(对象形式)
│ └── orderBy: { id: 'asc' }
│ 不推荐:不能动态添加字段
│
├── 单字段排序(数组形式)
│ └── orderBy: [{ id: 'asc' }]
│ 推荐:可以动态添加字段
│
└── 多字段排序(数组形式)
└── orderBy: [{ id: 'asc' }, { createdAt: 'desc' }]
推荐:支持多字段排序7.2 Service 层 orderBy 处理
typescript
// 正确:使用数组形式
async getCoursesByType(dto: GetCoursesByTypeDto) {
const orderBy = dto.order ? [dto.order] : [{ order: 'asc' as const }];
return this.prisma.courseTypes.findMany({
skip,
take,
orderBy, // 数组形式
});
}
// 错误:使用对象形式
async getCoursesByType(dto: GetCoursesByTypeDto) {
const orderBy = dto.order || { order: 'asc' };
return this.prisma.courseTypes.findMany({
skip,
take,
orderBy, // 对象形式(不支持动态添加字段)
});
}7.3 orderBy 常见错误
typescript
// 错误一:传递对象而不是数组
// 错误信息:orderBy must be an array, not an object
const orderBy = { id: 'asc' }; // 对象
// 正确:传递数组
const orderBy = [{ id: 'asc' }]; // 数组
// 错误二:多字段排序使用对象
const orderBy = {
id: 'asc',
createdAt: 'desc', // 不能添加多个字段
};
// 正确:多字段排序使用数组
const orderBy = [
{ id: 'asc' },
{ createdAt: 'desc' },
];八、完整实战示例
8.1 项目结构
code
project/
├── src/
│ ├── common/
│ │ └── dto/
│ │ ├── pagination.dto.ts
│ │ └── order-type.dto.ts
│ ├── modules/
│ │ └── course/
│ │ ├── dto/
│ │ │ └── get-courses-by-type.dto.ts
│ │ ├── course.controller.ts
│ │ ├── course.service.ts
│ │ └── course.module.ts
│ └── main.ts
└── package.json8.2 完整 PaginationDto 实现
typescript
// src/common/dto/pagination.dto.ts
import { IsNumber, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
import { OrderType } from './order-type.dto';
/**
* 分页查询通用参数
*/
export class PaginationDto {
/**
* 页码(从 1 开始)
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
page: number = 1;
/**
* 每页数量
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
size: number = 10;
/**
* 排序规则
* 格式:{ [fieldName]: 'asc' | 'desc' }
*/
@IsOptional()
order?: OrderType;
}8.3 完整 Service 实现
typescript
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
/**
* 根据分类查询课程(带分页和排序)
*/
async getCoursesByType(dto: GetCoursesByTypeDto) {
// 1. 计算分页参数
const skip = dto.page ? (dto.page - 1) * (dto.size || 10) : 0;
const take = dto.size || 10;
// 2. 处理排序参数(数组形式)
const orderBy = dto.order ? [dto.order] : [{ order: 'asc' as const }];
// 3. 执行查询
return this.prisma.courseTypes.findMany({
skip,
take,
orderBy,
include: {
tags: {
include: {
courses: {
include: {
course: {
include: {
author: true,
},
},
},
},
},
},
},
});
}
}8.4 完整 Controller 实现
typescript
// src/modules/course/course.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { CourseService } from './course.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';
@Controller('courses')
export class CourseController {
constructor(private readonly courseService: CourseService) {}
/**
* 根据分类查询课程(带分页和排序)
* GET /courses/by-type?page=1&size=10&order[id]=asc
*/
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
return this.courseService.getCoursesByType(dto);
}
}8.5 main.ts 全局配置
typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局启用 ValidationPipe 并开启 transform
app.useGlobalPipes(
new ValidationPipe({
transform: true, // 启用数据转换
whitelist: true, // 过滤掉未在 DTO 中定义的属性
}),
);
await app.listen(3000);
console.log('Application is running on: http://localhost:3000');
}
bootstrap();九、Postman 测试示例
9.1 测试一:基础分页查询
code
请求配置:
│
├── Method: GET
├── URL: http://localhost:3000/courses/by-type
│
├── Query Params:
│ ├── page: 1
│ └── size: 2
│
├── 实际 URL
│ └── ?page=1&size=2
│
└── 预期响应
├── 返回 2 条数据
└── page 和 size 都是 number 类型9.2 测试二:分页 + 排序查询
code
请求配置:
│
├── Method: GET
├── URL: http://localhost:3000/courses/by-type
│
├── Query Params:
│ ├── page: 1
│ ├── size: 2
│ ├── Key: order[id]
│ └── Value: asc
│
├── 实际 URL
│ └── ?page=1&size=2&order[id]=asc
│
└── 预期响应
├── 返回 2 条数据
├── 按 id 升序排列
└── 第一页:id=10, id=9(降序)9.3 测试三:验证分页正确性
code
分页验证流程:
│
├── 第一页(page=1, size=2)
│ ├── 返回 2 条数据
│ └── id: 10, 9
│
├── 第二页(page=2, size=2)
│ ├── 返回 2 条数据
│ └── id: 11, 12
│
├── 第三页(page=3, size=2)
│ ├── 返回 1 条数据
│ └── id: 13
│
└── 验证
├── 三页数据不重复
└── 总数据量正确9.4 测试四:多字段排序
code
请求配置:
│
├── Method: GET
├── URL: http://localhost:3000/courses/by-type
│
├── Query Params:
│ ├── page: 1
│ ├── size: 10
│ ├── Key: order[id]
│ ├── Value: asc
│ ├── Key: order[createdAt]
│ └── Value: desc
│
├── 实际 URL
│ └── ?page=1&size=10&order[id]=asc&order[createdAt]=desc
│
└── 预期响应
└── 先按 id 升序,再按 createdAt 降序十、常见问题与解决方案
10.1 Query 参数类型问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 参数类型是 string | Query 参数默认都是 string | 使用 @Type(() => Number) |
| 分页参数校验失败 | 类型不匹配 | 全局启用 transform: true |
| 排序参数解析失败 | 嵌套对象格式复杂 | 使用 qs 库或正确的格式 |
10.2 orderBy 格式错误
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
| orderBy must be an array | 传递了对象 | 改为数组形式 [{ id: 'asc' }] |
| 排序不生效 | 参数格式错误 | 检查 order 参数格式 |
| 多字段排序失败 | 对象形式不支持 | 使用数组形式 |
10.3 Transform 不生效问题
typescript
// 问题:未启用 transform
app.useGlobalPipes(new ValidationPipe());
// 解决:启用 transform
app.useGlobalPipes(
new ValidationPipe({
transform: true, // 必须启用
}),
);10.4 @Type 装饰器位置错误
typescript
// 错误:@Type 放在 @IsNumber 后面
@IsNumber()
@Type(() => Number) // 位置错误
// 正确:@Type 放在 @IsNumber 前面
@Type(() => Number) // 位置正确
@IsNumber()十一、最佳实践总结
11.1 分页查询最佳实践
code
分页查询最佳实践:
│
├── DTO 设计
│ ├── 通用分页参数放在 PaginationDto
│ ├── 业务 DTO 继承 PaginationDto
│ └── 使用 @Type 装饰器转换类型
│
├── Service 层
│ ├── 计算 skip 和 take
│ ├── 使用数组形式的 orderBy
│ └── 添加默认排序规则
│
├── Controller 层
│ ├── 使用 @Query() 接收参数
│ └── 返回 Service 层查询结果
│
└── 全局配置
├── 启用 transform: true
└── 启用 whitelist: true11.2 排序参数最佳实践
code
排序参数最佳实践:
│
├── 格式选择
│ ├── 推荐使用数组形式
│ └── 不推荐使用对象形式
│
├── 默认值设置
│ ├── 提供默认排序字段
│ └── 避免空 orderBy 报错
│
└── 字段白名单
├── 校验排序字段是否合法
└── 防止 SQL 注入(后续实现)11.3 性能优化建议
code
分页查询性能优化:
│
├── 数据库层面
│ ├── 添加索引(排序字段)
│ ├── 避免 SELECT *
│ └── 使用 select 选择字段
│
├── 应用层面
│ ├── 限制最大 size(如 100)
│ ├── 添加缓存
│ └── 使用游标分页(大数据量)
│
└── 前端层面
├── 虚拟列表渲染
├── 上拉加载更多
└── 防抖节流十二、命令速查表
12.1 Prisma 分页命令速查
| 操作 | 命令 | 说明 |
|---|---|---|
| 基础查询 | findMany() | 查询多条记录 |
| 分页查询 | findMany({ skip, take }) | 分页查询 |
| 排序查询 | findMany({ orderBy: [{ id: 'asc' }] }) | 排序查询 |
| 分页 + 排序 | findMany({ skip, take, orderBy }) | 分页排序查询 |
12.2 NestJS 装饰器速查
| 装饰器 | 说明 | 示例 |
|---|---|---|
@Query() | 接收 Query 参数 | @Query() dto: PaginationDto |
@Type(() => Number) | 转换为 Number | @Type(() => Number) page: number |
@IsNumber() | 校验 Number 类型 | @IsNumber() page: number |
@IsOptional() | 可选字段 | @IsOptional() page?: number |
12.3 分页参数速查
| 参数 | 公式 | 说明 |
|---|---|---|
| skip | (page - 1) * size | 跳过多少条 |
| take | size | 取多少条 |
| page=1, size=10 | skip=0, take=10 | 第一页 |
| page=2, size=10 | skip=10, take=10 | 第二页 |
| page=3, size=10 | skip=20, take=10 | 第三页 |
十三、扩展预告:自定义校验规则
13.1 OrderType 校验问题
code
OrderType 校验问题:
│
├── 问题
│ ├── 动态键值无法使用装饰器
│ ├── @ValidateNested 无法校验
│ └── 需要校验 value 是 'asc' 或 'desc'
│
├── 解决方案(下节课讲解)
│ ├── 自定义校验装饰器
│ ├── 自定义校验规则
│ └── 实现 OrderType 校验
│
└── 预告
└── 下节课单独讲解自定义校验规则13.2 自定义校验规则示例
typescript
// 预告:下节课实现
import { registerDecorator, ValidationOptions } from 'class-validator';
export function IsOrderType(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isOrderType',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any) {
// 校验逻辑:value 必须是 { [key: string]: 'asc' | 'desc' }
return true;
},
},
});
};
}十四、学习要点总结
14.1 核心知识点
code
本文核心要点:
│
├── 分页查询通用参数
│ ├── PaginationDto 定义
│ ├── page、size、order 三个参数
│ └── DTO 继承复用
│
├── Transform 数据转换
│ ├── 全局启用 transform: true
│ ├── 使用 @Type(() => Number) 转换类型
│ └── Query 参数自动转换
│
├── orderBy 数组形式
│ ├── 必须使用数组形式 [{ id: 'asc' }]
│ ├── 支持多字段排序
│ └── 避免 Prisma 报错
│
├── qs 库使用
│ ├── 解析复杂 Query 参数
│ ├── 嵌套对象格式
│ └── order[id]=asc
│
└── Service 层实现
├── 计算 skip 和 take
├── 处理 orderBy 参数
└── 添加默认值14.2 重要程度标注
| 知识点 | 重要程度 | 说明 |
|---|---|---|
| PaginationDto 定义 | 必须掌握 | 分页查询基础 |
| @Type 装饰器 | 必须掌握 | 类型转换核心 |
| orderBy 数组形式 | 必须掌握 | 排序功能实现 |
| 全局启用 transform | 必须掌握 | 数据转换配置 |
| qs 库使用 | 重要 | 复杂参数解析 |
| 自定义校验规则 | 了解 | 下节课讲解 |
14.3 学习路径规划
code
学习路径规划:
│
├── 第一阶段:理解概念(1 天)
│ ├── 理解分页查询原理
│ ├── 理解 Transform 机制
│ └── 理解 orderBy 格式
│
├── 第二阶段:实践操作(2-3 天)
│ ├── 实现 PaginationDto
│ ├── 实现分页查询接口
│ └── 测试分页和排序功能
│
└── 第三阶段:深入应用(持续)
├── 实现自定义校验规则
├── 优化查询性能
└── 添加缓存机制十五、完整代码清单
15.1 PaginationDto 完整代码
typescript
// src/common/dto/pagination.dto.ts
import { IsNumber, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
import { OrderType } from './order-type.dto';
/**
* 分页查询通用参数
*/
export class PaginationDto {
/**
* 页码(从 1 开始)
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
page: number = 1;
/**
* 每页数量
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
size: number = 10;
/**
* 排序规则
* 格式:{ [fieldName]: 'asc' | 'desc' }
* 示例:{ id: 'asc', createdAt: 'desc' }
*/
@IsOptional()
order?: OrderType;
}15.2 OrderType 完整代码
typescript
// src/common/dto/order-type.dto.ts
import 'reflect-metadata';
/**
* 排序类型
* key: 字段名
* value: 排序方式(asc 升序 / desc 降序)
*/
export class OrderType {
[key: string]: 'asc' | 'desc';
}15.3 GetCoursesByTypeDto 完整代码
typescript
// src/modules/course/dto/get-courses-by-type.dto.ts
import { PaginationDto } from '@/common/dto/pagination.dto';
/**
* 根据分类查询课程 DTO
* 继承分页通用参数
*/
export class GetCoursesByTypeDto extends PaginationDto {
// 可以添加其他特定参数
// 例如:typeId?: number;
}15.4 CourseService 完整代码
typescript
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
/**
* 根据分类查询课程(带分页和排序)
*/
async getCoursesByType(dto: GetCoursesByTypeDto) {
// 1. 计算分页参数
const skip = dto.page ? (dto.page - 1) * (dto.size || 10) : 0;
const take = dto.size || 10;
// 2. 处理排序参数(数组形式)
const orderBy = dto.order ? [dto.order] : [{ order: 'asc' as const }];
// 3. 执行查询
return this.prisma.courseTypes.findMany({
skip,
take,
orderBy,
include: {
tags: {
include: {
courses: {
include: {
course: {
include: {
author: true,
},
},
},
},
},
},
},
});
}
}15.5 CourseController 完整代码
typescript
// src/modules/course/course.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { CourseService } from './course.service';
import { GetCoursesByTypeDto } from './dto/get-courses-by-type.dto';
@Controller('courses')
export class CourseController {
constructor(private readonly courseService: CourseService) {}
/**
* 根据分类查询课程(带分页和排序)
* GET /courses/by-type?page=1&size=10&order[id]=asc
*/
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
return this.courseService.getCoursesByType(dto);
}
}重要提示:分页查询是后端开发的基础功能,掌握 PaginationDto 的定义、Transform 数据转换、orderBy 数组形式、qs 库使用,对实际项目开发非常重要!特别是要注意 Query 参数默认是 string 类型,必须使用 @Type(() => Number) 转换,并且 orderBy 必须使用数组形式!
下节课预告:自定义校验规则,实现 OrderType 的 value 字段校验('asc' | 'desc')!