{T}

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 分页参数说明表

参数类型默认值说明
pagenumber1页码,从 1 开始
sizenumber10每页数量
orderOrderTypeundefined排序规则,格式:{ 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 PaginationDto

3.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.json

8.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 参数类型问题

问题原因解决方案
参数类型是 stringQuery 参数默认都是 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: true

11.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跳过多少条
takesize取多少条
page=1, size=10skip=0, take=10第一页
page=2, size=10skip=10, take=10第二页
page=3, size=10skip=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')!