NestJS课程筛选与数组参数传递实战
学习目标:掌握数组参数传递的两种方式、@Matches 正则校验、Prisma where 子句使用、DTO 类型转换技巧。
一、课程筛选需求背景
1.1 业务场景描述
code
课程筛选业务场景:
│
├── 问题背景
│ ├── 分类越来越多
│ ├── 前端需要限制显示的分类
│ ├── 接口需要限制响应的分类
│ └── 不同页面展示不同的分类
│
├── 解决方案
│ ├── 前端传递 types 参数(数组)
│ ├── 后端根据 types 筛选分类
│ ├── 只返回指定 ID 的分类数据
│ └── 支持多分类同时筛选
│
└── 实现效果
├── 传递 types=11,12,13
├── 只返回 ID 为 11、12、13 的分类
└── 未传递则返回所有分类1.2 Query 参数传递数组的两种方式
code
Query 参数传递数组的两种方式:
│
├── 方式一:逗号分隔字符串
│ ├── 格式:?types=11,12,13
│ ├── 优点:URL 简洁
│ ├── 缺点:需要手动转换
│ └── 校验:使用 @Matches 正则校验
│
└── 方式二:数组形式(推荐)
│ ├── 格式:?types[0]=11&types[1]=12
│ ├── 优点:自动转换类型
│ ├── 缺点:URL 较长
│ └── 校验:使用 @IsNumber({ each: true })二、方式一:逗号分隔字符串
2.1 DTO 定义(方式一)
typescript
// src/modules/course/dto/get-courses-by-type.dto.ts
import { IsString, IsOptional, Matches } from 'class-validator';
import { PaginationDto } from '@/common/dto/pagination.dto';
/**
* 根据分类查询课程 DTO(方式一:逗号分隔字符串)
*/
export class GetCoursesByTypeDto extends PaginationDto {
/**
* 分类 ID 列表(逗号分隔的字符串)
* 格式:11,12,13
*/
@IsString()
@IsOptional()
@Matches(/^\d+(,\d+)*$/, {
message: 'types 必须是逗号分隔的数字字符串,如:11,12,13',
})
types?: string;
}2.2 @Matches 正则装饰器详解
code
@Matches 正则装饰器详解:
│
├── 正则表达式:/^\d+(,\d+)*$/
│ ├── ^:字符串开始
│ ├── \d+:一个或多个数字
│ ├── (,\d+)*:逗号 + 数字,重复 0 次或多次
│ └── $:字符串结束
│
├── 校验规则
│ ├── 正确:11,12,13
│ ├── 正确:11
│ ├── 错误:11,12,abc
│ ├── 错误:,11,12
│ └── 错误:11,,12
│
└── 使用场景
├── 校验 ID 列表
├── 校验标签列表
└── 校验状态列表2.3 正则表达式生成技巧
code
使用 AI 生成正则表达式:
│
├── 工具:Copilot、ChatGPT、Claude
│
├── 提示词示例
│ "帮我写一个正则来校验一个字符串,格式是像 1,2,3,4,5 用逗号隔开的数字,
│ 不能有数字以外的内容,因为我要校验 ID 数据"
│
├── 生成的正则
│ /^\d+(,\d+)*$/
│
└── 验证测试
├── 测试通过:11,12,13
├── 测试通过:1
└── 测试失败:11,abc,132.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&types=11,12,13
*/
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
// 1. 转换 types 参数
const newDto = {
...dto,
types: dto.types
?.split(',')
.map((id) => parseInt(id, 10))
.filter((id) => !isNaN(id)),
};
// 2. 调用 Service
return this.courseService.getCoursesByType(newDto);
}
}2.5 数据转换详解
code
数据转换流程:
│
├── 第一步:接收字符串
│ └── dto.types = '11,12,13'
│
├── 第二步:split 分割
│ └── '11,12,13'.split(',') → ['11', '12', '13']
│
├── 第三步:map 转换
│ └── ['11', '12', '13'].map(id => parseInt(id, 10))
│ → [11, 12, 13]
│
├── 第四步:filter 过滤
│ └── 过滤掉 NaN 值
│
└── 第五步:传递给 Service
└── newDto.types = [11, 12, 13]2.6 Service 层实现(方式一)
typescript
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
// 定义新的 DTO 类型(包含转换后的 types)
export class NewGetCoursesByTypeDto {
page?: number;
size?: number;
order?: any;
types?: number[];
}
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
async getCoursesByType(dto: NewGetCoursesByTypeDto) {
// 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({
where: dto.types ? { id: { in: dto.types } } : undefined,
skip,
take,
orderBy,
include: {
tags: {
include: {
courses: {
include: {
course: {
include: {
author: true,
},
},
},
},
},
},
},
});
}
}2.7 Prisma where 子句详解
code
Prisma where 子句详解:
│
├── 基本用法
│ └── where: { id: 1 }
│ 查询 id = 1 的记录
│
├── IN 查询
│ └── where: { id: { in: [1, 2, 3] } }
│ 查询 id 在 [1, 2, 3] 中的记录
│
├── NOT IN 查询
│ └── where: { id: { notIn: [1, 2, 3] } }
│ 查询 id 不在 [1, 2, 3] 中的记录
│
├── 条件组合
│ ├── AND: { AND: [{ id: 1 }, { status: 'active' }] }
│ ├── OR: { OR: [{ id: 1 }, { id: 2 }] }
│ └── NOT: { NOT: { id: 1 } }
│
└── 本节示例
└── where: dto.types ? { id: { in: dto.types } } : undefined
如果传递了 types,则筛选;否则不过滤三、方式二:数组形式(推荐)
3.1 DTO 定义(方式二)
typescript
// src/modules/course/dto/get-courses-by-type.dto.ts
import { IsNumber, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
import { PaginationDto } from '@/common/dto/pagination.dto';
/**
* 根据分类查询课程 DTO(方式二:数组形式)
*/
export class GetCoursesByTypeDto extends PaginationDto {
/**
* 分类 ID 列表(数组)
* 格式:[11, 12, 13]
*/
@IsNumber({}, { each: true })
@IsOptional()
@Type(() => Number)
types?: number[];
}3.2 @IsNumber({ each: true }) 详解
code
@IsNumber({ each: true }) 详解:
│
├── 作用
│ ├── 校验数组中的每个元素是否为 Number 类型
│ └── 类似于遍历数组,对每个元素应用 @IsNumber()
│
├── 语法
│ ├── 第一个参数:IsNumberOptions(空对象 {})
│ └── 第二个参数:ValidationOptions({ each: true })
│
├── 等价代码
│ @IsNumber({}, { each: true })
│ types?: number[];
│
│ 等价于:
│ types.forEach(item => {
│ @IsNumber()(item);
│ });
│
└── 使用场景
├── 校验数字数组
├── 校验字符串数组
└── 校验对象数组3.3 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&types[0]=11&types[1]=12
*/
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
// 直接传递给 Service,无需转换
return this.courseService.getCoursesByType(dto);
}
}3.4 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({
where: dto.types ? { id: { in: dto.types } } : undefined,
skip,
take,
orderBy,
include: {
tags: {
include: {
courses: {
include: {
course: {
include: {
author: true,
},
},
},
},
},
},
},
});
}
}四、两种方式对比
4.1 实现复杂度对比
| 维度 | 方式一:逗号分隔 | 方式二:数组形式 |
|---|---|---|
| DTO 定义 | 需要正则校验 | 使用 each: true |
| Controller | 需要手动转换 | 无需转换 |
| Service | 需要新 DTO 类型 | 直接使用原 DTO |
| 代码量 | 较多 | 较少 |
| 学习成本 | 低 | 中 |
4.2 URL 格式对比
code
URL 格式对比:
│
├── 方式一:逗号分隔
│ ├── ?types=11,12,13
│ ├── 优点:URL 简洁
│ └── 缺点:可读性一般
│
└── 方式二:数组形式
├── ?types[0]=11&types[1]=12&types[2]=13
├── 优点:可读性好
└── 缺点:URL 较长4.3 数据转换对比
| 维度 | 方式一:逗号分隔 | 方式二:数组形式 |
|---|---|---|
| 转换时机 | Controller 层 | DTO 层(自动) |
| 转换方式 | split + parseInt + filter | @Type(() => Number) |
| 校验方式 | @Matches 正则 | @IsNumber({ each: true }) |
| 错误提示 | 正则不匹配 | 类型不匹配 |
4.4 推荐选择
code
推荐选择建议:
│
├── 推荐使用方式二:数组形式
│ ├── 优点:代码简洁、自动转换、类型安全
│ ├── 缺点:URL 稍长
│ └── 适用:大多数场景
│
└── 方式一:逗号分隔
├── 优点:URL 简洁
├── 缺点:需要手动转换、校验复杂
└── 适用:对 URL 长度敏感的场景五、Postman 测试示例
5.1 测试一:逗号分隔方式
code
请求配置(方式一):
│
├── Method: GET
├── URL: http://localhost:3000/courses/by-type
│
├── Query Params:
│ ├── page: 1
│ ├── size: 10
│ └── types: 11,12,13
│
├── 实际 URL
│ └── ?page=1&size=10&types=11,12,13
│
└── 预期响应
├── 状态码:200
└── 返回 ID 为 11、12、13 的分类5.2 测试二:数组形式方式
code
请求配置(方式二):
│
├── Method: GET
├── URL: http://localhost:3000/courses/by-type
│
├── Query Params:
│ ├── page: 1
│ ├── size: 10
│ ├── Key: types[0], Value: 11
│ ├── Key: types[1], Value: 12
│ └── Key: types[2], Value: 13
│
├── 实际 URL
│ └── ?page=1&size=10&types[0]=11&types[1]=12&types[2]=13
│
└── 预期响应
├── 状态码:200
└── 返回 ID 为 11、12、13 的分类5.3 测试三:校验失败
code
校验失败测试(方式一):
│
├── Query Params
│ └── types: 11,abc,13
│
└── 预期响应
├── 状态码:400
└── 错误消息:
{
"statusCode": 400,
"message": ["types 必须是逗号分隔的数字字符串,如:11,12,13"],
"error": "Bad Request"
}
校验失败测试(方式二):
│
├── Query Params
│ ├── types[0]: 11
│ └── types[1]: abc
│
└── 预期响应
├── 状态码:400
└── 错误消息:
{
"statusCode": 400,
"message": ["每个 types 必须是数字类型"],
"error": "Bad Request"
}5.4 测试四:单 ID 筛选
code
请求配置:
│
├── 方式一
│ └── ?types=11
│
├── 方式二
│ └── ?types[0]=11
│
└── 预期响应
├── 状态码:200
└── 只返回 ID 为 11 的分类六、完整实战示例
6.1 项目结构
code
project/
├── src/
│ ├── common/
│ │ ├── decorators/
│ │ │ └── is-valid-value-in-arr.decorator.ts
│ │ └── dto/
│ │ └── pagination.dto.ts
│ ├── modules/
│ │ └── course/
│ │ ├── dto/
│ │ │ └── get-courses-by-type.dto.ts
│ │ ├── course.controller.ts
│ │ ├── course.service.ts
│ │ └── course.module.ts
│ └── main.ts
└── package.json6.2 完整 DTO 实现(方式一:逗号分隔)
typescript
// src/modules/course/dto/get-courses-by-type.dto.ts
import { IsString, IsOptional, Matches } from 'class-validator';
import { PaginationDto } from '@/common/dto/pagination.dto';
/**
* 根据分类查询课程 DTO(方式一:逗号分隔字符串)
*/
export class GetCoursesByTypeDto extends PaginationDto {
/**
* 分类 ID 列表(逗号分隔的字符串)
* 格式:11,12,13
*/
@IsString()
@IsOptional()
@Matches(/^\d+(,\d+)*$/, {
message: 'types 必须是逗号分隔的数字字符串,如:11,12,13',
})
types?: string;
}6.3 完整 DTO 实现(方式二:数组形式)
typescript
// src/modules/course/dto/get-courses-by-type.dto.ts
import { IsNumber, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
import { PaginationDto } from '@/common/dto/pagination.dto';
/**
* 根据分类查询课程 DTO(方式二:数组形式)
*/
export class GetCoursesByTypeDto extends PaginationDto {
/**
* 分类 ID 列表(数组)
* 格式:[11, 12, 13]
*/
@IsNumber({}, { each: true })
@IsOptional()
@Type(() => Number)
types?: number[];
}6.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&types=11,12,13
*/
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
// 转换 types 参数
const newDto = {
...dto,
types: dto.types
?.split(',')
.map((id) => parseInt(id, 10))
.filter((id) => !isNaN(id)),
};
return this.courseService.getCoursesByType(newDto);
}
}6.5 完整 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&types[0]=11&types[1]=12
*/
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
return this.courseService.getCoursesByType(dto);
}
}6.6 完整 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({
where: dto.types ? { id: { in: dto.types } } : undefined,
skip,
take,
orderBy,
include: {
tags: {
include: {
courses: {
include: {
course: {
include: {
author: true,
},
},
},
},
},
},
},
});
}
}七、常见问题与解决方案
7.1 正则校验不生效问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 正则不匹配 | 正则表达式写错 | 使用 AI 工具生成正则 |
| 空字符串通过 | 正则未考虑空值 | 添加 @IsNotEmpty() 或调整正则 |
| 逗号位置错误 | 正则未限制位置 | 使用 ^ 和 $ 限制首尾 |
7.2 数组参数转换问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 数组为 string[] | 未使用 @Type 转换 | 添加 @Type(() => Number) |
| 转换后为 NaN | 前端传递非数字 | 使用 filter 过滤 NaN |
| each: true 不生效 | 参数位置错误 | @IsNumber({}, { each: true }) |
7.3 Prisma where 条件问题
typescript
// 错误:空数组会查询不到任何数据
where: { id: { in: [] } }
// 正确:空数组不过滤
where: dto.types?.length > 0 ? { id: { in: dto.types } } : undefined
// 错误:undefined 导致查询失败
where: { id: { in: undefined } }
// 正确:undefined 不添加条件
where: dto.types ? { id: { in: dto.types } } : undefined八、最佳实践总结
8.1 数组参数传递最佳实践
code
数组参数传递最佳实践:
│
├── 推荐使用方式二(数组形式)
│ ├── 优点:代码简洁、类型安全
│ ├── 使用 @IsNumber({}, { each: true }) 校验
│ └── 使用 @Type(() => Number) 转换
│
├── 校验要点
│ ├── 空值处理:@IsOptional()
│ ├── 类型校验:@IsNumber({ each: true })
│ └── 数组长度:@ArrayMaxSize(100)
│
└── 错误处理
├── 提供友好的错误消息
├── 过滤无效的值
└── 记录校验失败日志8.2 Prisma where 条件最佳实践
code
Prisma where 条件最佳实践:
│
├── 条件判断
│ ├── 空值判断:dto.types ? ... : undefined
│ ├── 空数组判断:dto.types?.length > 0 ? ... : undefined
│ └── 多条件组合:使用 AND、OR
│
├── IN 查询优化
│ ├── 避免空数组:检查 length
│ ├── 限制数量:@ArrayMaxSize(100)
│ └── 添加索引:数据库索引优化
│
└── 性能优化
├── 使用 select 选择字段
├── 使用分页查询
└── 添加必要的索引8.3 DTO 设计最佳实践
code
DTO 设计最佳实践:
│
├── 类型定义
│ ├── 继承通用 DTO
│ ├── 使用 TypeScript 类型
│ └── 添加 JSDoc 注释
│
├── 校验规则
│ ├── 使用 class-validator 装饰器
│ ├── 提供清晰的错误消息
│ └── 考虑边界情况
│
└── 转换处理
├── 使用 class-transformer 装饰器
├── 在 DTO 层完成转换
└── 避免在 Service 层手动转换九、命令速查表
9.1 数组参数传递命令速查
| 操作 | 方式一 | 方式二 |
|---|---|---|
| URL 格式 | ?types=11,12,13 | ?types[0]=11&types[1]=12 |
| DTO 类型 | types?: string | types?: number[] |
| 校验装饰器 | @Matches(/^\d+(,\d+)*$/) | @IsNumber({}, { each: true }) |
| 转换方式 | split(',').map(parseInt) | @Type(() => Number) |
| 转换位置 | Controller 层 | DTO 层(自动) |
9.2 Prisma where 条件速查
| 操作 | 代码 | 说明 |
|---|---|---|
| IN 查询 | { id: { in: [1, 2, 3] } } | id 在数组中 |
| NOT IN 查询 | { id: { notIn: [1, 2, 3] } } | id 不在数组中 |
| 条件判断 | dto.types ? { id: { in: dto.types } } : undefined | 有值则过滤 |
| 空数组处理 | dto.types?.length > 0 ? ... : undefined | 空数组不过滤 |
9.3 正则表达式速查
| 场景 | 正则表达式 | 说明 |
|---|---|---|
| 逗号分隔数字 | /^\d+(,\d+)*$/ | 11,12,13 |
| 分号分隔数字 | /^\d+(;\d+)*$/ | 11;12;13 |
| 逗号或分号分隔 | /^\d+([,;]\d+)*$/ | 11,12;13 |
| 允许空格 | /^\d+(,\s*\d+)*$/ | 11, 12, 13 |
十、扩展应用场景
10.1 其他筛选场景
typescript
// 示例一:标签筛选
export class GetCoursesByTagDto extends PaginationDto {
@IsNumber({}, { each: true })
@IsOptional()
@Type(() => Number)
tagIds?: number[];
}
// 示例二:作者筛选
export class GetCoursesByAuthorDto extends PaginationDto {
@IsNumber({}, { each: true })
@IsOptional()
@Type(() => Number)
authorIds?: number[];
}
// 示例三:状态筛选
export class GetCoursesByStatusDto extends PaginationDto {
@IsString({ each: true })
@IsOptional()
statuses?: string[];
}10.2 多条件组合筛选
typescript
// 多条件组合筛选
export class GetCoursesFilterDto extends PaginationDto {
@IsNumber({}, { each: true })
@IsOptional()
@Type(() => Number)
typeIds?: number[];
@IsNumber({}, { each: true })
@IsOptional()
@Type(() => Number)
tagIds?: number[];
@IsNumber({}, { each: true })
@IsOptional()
@Type(() => Number)
authorIds?: number[];
}
// Service 层实现
async getCoursesWithFilter(dto: GetCoursesFilterDto) {
return this.prisma.courses.findMany({
where: {
AND: [
dto.typeIds ? { typeId: { in: dto.typeIds } } : {},
dto.tagIds ? { tags: { some: { id: { in: dto.tagIds } } } } : {},
dto.authorIds ? { authorId: { in: dto.authorIds } } : {},
],
},
});
}10.3 前端使用 qs 库示例
typescript
// 前端使用 qs 库
import qs from 'qs';
const params = {
page: 1,
size: 10,
types: [11, 12, 13],
};
// 转换为 URL 参数
const queryString = qs.stringify(params);
// 输出:page=1&size=10&types[0]=11&types[1]=12&types[2]=13
// 发送请求
axios.get(`/courses/by-type?${queryString}`);十一、学习要点总结
11.1 核心知识点
code
本文核心要点:
│
├── 数组参数传递两种方式
│ ├── 方式一:逗号分隔字符串
│ └── 方式二:数组形式(推荐)
│
├── @Matches 正则校验
│ ├── 校验逗号分隔的数字字符串
│ ├── 使用 AI 工具生成正则
│ └── 提供清晰的错误消息
│
├── @IsNumber({ each: true })
│ ├── 校验数组中每个元素
│ ├── 配合 @Type(() => Number) 转换
│ └── 自动类型转换
│
├── Prisma where 子句
│ ├── IN 查询:{ id: { in: [1, 2, 3] } }
│ ├── 条件判断:dto.types ? ... : undefined
│ └── 空数组处理:length > 0 判断
│
└── 最佳实践
├── 推荐使用数组形式
├── 在 DTO 层完成转换
└── 提供友好的错误消息11.2 重要程度标注
| 知识点 | 重要程度 | 说明 |
|---|---|---|
| @IsNumber({ each: true }) | 必须掌握 | 数组校验核心 |
| @Type(() => Number) | 必须掌握 | 类型转换核心 |
| Prisma where 条件 | 必须掌握 | 数据筛选核心 |
| @Matches 正则校验 | 重要 | 字符串校验 |
| qs 库使用 | 了解 | 前端工具 |
11.3 学习路径规划
code
学习路径规划:
│
├── 第一阶段:理解概念(1 天)
│ ├── 理解数组参数传递两种方式
│ ├── 理解 Prisma where 条件
│ └── 理解正则校验原理
│
├── 第二阶段:实践操作(2-3 天)
│ ├── 实现逗号分隔方式
│ ├── 实现数组形式方式
│ └── 测试两种方式对比
│
└── 第三阶段:深入应用(持续)
├── 多条件组合筛选
├── 性能优化
└── 前端集成十二、完整代码清单
12.1 方式一完整代码
typescript
// src/modules/course/dto/get-courses-by-type.dto.ts
import { IsString, IsOptional, Matches } from 'class-validator';
import { PaginationDto } from '@/common/dto/pagination.dto';
export class GetCoursesByTypeDto extends PaginationDto {
@IsString()
@IsOptional()
@Matches(/^\d+(,\d+)*$/, {
message: 'types 必须是逗号分隔的数字字符串,如:11,12,13',
})
types?: string;
}
// src/modules/course/course.controller.ts
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
const newDto = {
...dto,
types: dto.types
?.split(',')
.map((id) => parseInt(id, 10))
.filter((id) => !isNaN(id)),
};
return this.courseService.getCoursesByType(newDto);
}12.2 方式二完整代码
typescript
// src/modules/course/dto/get-courses-by-type.dto.ts
import { IsNumber, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
import { PaginationDto } from '@/common/dto/pagination.dto';
export class GetCoursesByTypeDto extends PaginationDto {
@IsNumber({}, { each: true })
@IsOptional()
@Type(() => Number)
types?: number[];
}
// src/modules/course/course.controller.ts
@Get('by-type')
async getCoursesByType(@Query() dto: GetCoursesByTypeDto) {
return this.courseService.getCoursesByType(dto);
}12.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) {
const skip = dto.page ? (dto.page - 1) * (dto.size || 10) : 0;
const take = dto.size || 10;
const orderBy = dto.order ? [dto.order] : [{ order: 'asc' as const }];
return this.prisma.courseTypes.findMany({
where: dto.types ? { id: { in: dto.types } } : undefined,
skip,
take,
orderBy,
include: {
tags: {
include: {
courses: {
include: {
course: {
include: {
author: true,
},
},
},
},
},
},
},
});
}
}重要提示:数组参数传递是后端开发的常见需求,掌握两种传递方式的实现和校验,理解 Prisma where 条件的使用,对实际项目开发非常重要!推荐使用方式二(数组形式),代码更简洁、类型更安全、维护性更好!
扩展建议:可以尝试实现多条件组合筛选、标签筛选、作者筛选等功能,进一步掌握数组参数传递和 Prisma where 条件的应用!