feat(diet-records): 修复营养成分分析记录查询参数验证和类型转换
修复GET请求查询参数验证装饰器缺失问题,添加正确的class-validator装饰器 在控制器中实现查询参数类型转换,确保数字参数正确处理 更新技术文档,添加DTO验证装饰器编写规范和GET请求参数处理指南
This commit is contained in:
@@ -254,6 +254,121 @@ NODE_ENV=development
|
||||
- **SQL 注入防护**: Sequelize ORM 自动防护
|
||||
- **XSS 防护**: 输入清理和输出编码
|
||||
|
||||
#### DTO 验证装饰器编写规范
|
||||
|
||||
**基本原则**:
|
||||
- 所有 DTO 类必须同时包含 `@ApiProperty` 和 `class-validator` 装饰器
|
||||
- `@ApiProperty` 用于 Swagger 文档生成,`class-validator` 用于数据验证
|
||||
- 缺少验证装饰器会导致参数校验失败,使 API 端点无法正常工作
|
||||
|
||||
**必需要导入的验证装饰器**:
|
||||
```typescript
|
||||
import { IsOptional, IsDateString, IsNumber, IsString, IsNotEmpty, IsEnum, MaxLength, Min, Max } from 'class-validator';
|
||||
```
|
||||
|
||||
**常用字段验证规则**:
|
||||
- **分页参数**:在 GET 请求中使用 `@IsOptional()` + `@IsString()`,在控制器中转换为数字;在 POST/PUT 请求中使用 `@IsOptional()` + `@IsNumber()`
|
||||
- **日期参数**:`startDate` 和 `endDate` 使用 `@IsOptional()` + `@IsDateString()`
|
||||
- **字符串参数**:使用 `@IsOptional()` + `@IsString()`,必要时添加 `@MaxLength()`
|
||||
- **枚举参数**:使用 `@IsOptional()` + `@IsEnum(EnumType)`
|
||||
- **必填字段**:使用 `@IsNotEmpty()` 而不是只使用 `@IsString()`
|
||||
|
||||
#### GET 请求参数特殊处理
|
||||
|
||||
**重要说明**:GET 请求的查询参数(Query Parameters)在 HTTP 协议中都是字符串类型,即使看起来是数字(如 `?page=1&limit=20`)。因此需要特殊处理:
|
||||
|
||||
**GET 请求 DTO 定义**:
|
||||
```typescript
|
||||
export class GetRecordsQueryDto {
|
||||
@ApiProperty({ description: '页码', example: 1, required: false })
|
||||
@IsOptional()
|
||||
@IsString() // 注意:GET 请求中使用 @IsString() 而不是 @IsNumber()
|
||||
page?: string;
|
||||
|
||||
@ApiProperty({ description: '每页数量', example: 20, required: false })
|
||||
@IsOptional()
|
||||
@IsString() // 注意:GET 请求中使用 @IsString() 而不是 @IsNumber()
|
||||
limit?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**控制器中的类型转换**:
|
||||
```typescript
|
||||
async getRecords(
|
||||
@Query() query: GetRecordsQueryDto,
|
||||
@CurrentUser() user: AccessTokenPayload,
|
||||
): Promise<RecordsResponseDto> {
|
||||
// 转换查询参数中的字符串为数字
|
||||
const convertedQuery = {
|
||||
page: query.page ? parseInt(query.page, 10) : undefined,
|
||||
limit: query.limit ? parseInt(query.limit, 10) : undefined,
|
||||
// 其他字符串参数保持不变
|
||||
startDate: query.startDate,
|
||||
endDate: query.endDate,
|
||||
status: query.status,
|
||||
};
|
||||
|
||||
const result = await this.service.getRecords(user.sub, convertedQuery);
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
**POST/PUT 请求 DTO 定义**(对比):
|
||||
```typescript
|
||||
export class CreateRecordDto {
|
||||
@ApiProperty({ description: '数量', example: 10 })
|
||||
@IsNumber() // POST/PUT 请求中可以直接使用 @IsNumber()
|
||||
quantity: number;
|
||||
}
|
||||
```
|
||||
|
||||
**正确示例(GET 请求)**:
|
||||
```typescript
|
||||
export class GetRecordsQueryDto {
|
||||
@ApiProperty({ description: '页码', example: 1, required: false })
|
||||
@IsOptional()
|
||||
@IsString() // GET 请求中使用 @IsString()
|
||||
page?: string;
|
||||
|
||||
@ApiProperty({ description: '每页数量', example: 20, required: false })
|
||||
@IsOptional()
|
||||
@IsString() // GET 请求中使用 @IsString()
|
||||
limit?: string;
|
||||
|
||||
@ApiProperty({ description: '开始日期', example: '2023-01-01', required: false })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startDate?: string;
|
||||
|
||||
@ApiProperty({ description: '状态', example: 'active', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**正确示例(POST/PUT 请求)**:
|
||||
```typescript
|
||||
export class CreateRecordDto {
|
||||
@ApiProperty({ description: '数量', example: 10 })
|
||||
@IsNumber() // POST/PUT 请求中可以直接使用 @IsNumber()
|
||||
quantity: number;
|
||||
|
||||
@ApiProperty({ description: '名称', example: '测试' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
}
|
||||
```
|
||||
|
||||
**常见错误**:
|
||||
- ❌ 只有 `@ApiProperty` 而缺少 `class-validator` 装饰器
|
||||
- ❌ GET 请求中的数字参数使用 `@IsNumber()` 而不是 `@IsString()`
|
||||
- ❌ 使用 `@IsString()` 但没有 `@IsOptional()` 处理可选参数
|
||||
- ❌ 日期字段没有使用 `@IsDateString()` 验证
|
||||
- ❌ GET 请求中忘记在控制器进行类型转换,导致服务层接收到字符串而不是数字
|
||||
- ❌ 类型转换时没有进行空值检查,可能导致 `parseInt(undefined)` 返回 `NaN`
|
||||
|
||||
### 访问控制
|
||||
- **JWT 认证**: 无状态 Token 认证
|
||||
- **权限守卫**: 基于角色的访问控制
|
||||
|
||||
Reference in New Issue
Block a user