168 lines
5.6 KiB
Markdown
168 lines
5.6 KiB
Markdown
# Pydantic Schema规范
|
|
|
|
## 概述
|
|
|
|
使用 Pydantic v2 进行请求数据校验。所有请求体参数通过 Pydantic `BaseModel` 定义,Tornado 在调用 Endpoint 前自动完成解析和验证。
|
|
|
|
:::tip 核心原则
|
|
- 请求体参数必须声明为 Pydantic 模型(Form 类)
|
|
- 字段约束使用 `Field(...)` 定义
|
|
- 自定义校验使用 `@field_validator`
|
|
- 唯一性校验在 Schema 中查询数据库
|
|
:::
|
|
|
|
## 命名规范
|
|
|
|
| 类型 | 命名格式 | 示例 |
|
|
|------|---------|------|
|
|
| 创建/编辑表单 | `{模块}Form` | `PositionForm` |
|
|
| 状态更新表单 | `{模块}StatusForm` | `PositionStatusForm` |
|
|
| 查询参数表单 | `{模块}QueryForm` | `PositionQueryForm` |
|
|
|
|
:::tip 继承 BaseSchema
|
|
创建/编辑表单继承 `core.base_schema.BaseSchema`,自动获得 `id: Optional[int]` 字段(编辑时必填)。
|
|
:::
|
|
|
|
## PositionForm 示例
|
|
|
|
以岗位模块为例,`src/modules/position/schemas.py` 完整内容:
|
|
|
|
```python
|
|
from typing import Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
from core.base_schema import BaseSchema
|
|
|
|
|
|
class PositionForm(BaseSchema):
|
|
"""岗位创建/编辑表单"""
|
|
name: str = Field(..., min_length=1, max_length=150, description="岗位名称")
|
|
status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用")
|
|
sort: int = Field(..., ge=0, le=99999, description="岗位排序")
|
|
|
|
|
|
class PositionStatusForm(BaseModel):
|
|
"""岗位状态更新表单"""
|
|
id: int = Field(..., gt=0, description="岗位ID")
|
|
status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用")
|
|
```
|
|
|
|
:::tip BaseSchema 基类
|
|
`BaseSchema`(定义在 `src/core/base_schema.py`)提供 `id: Optional[int]` 字段和 `empty_str_to_none` 验证器,自动将前端传入的空字符串 `''`/`'null'`/`'undefined'` 转为 `None`,避免 Pydantic 整数解析报错。
|
|
:::
|
|
|
|
## Field 约束
|
|
|
|
| 约束 | 类型 | 说明 | 示例 |
|
|
|------|------|------|------|
|
|
| `min_length` | str | 最小长度 | `Field(..., min_length=1)` |
|
|
| `max_length` | str | 最大长度 | `Field(..., max_length=150)` |
|
|
| `ge` | int/float | 大于等于 | `Field(..., ge=0)` |
|
|
| `le` | int/float | 小于等于 | `Field(..., le=99999)` |
|
|
| `gt` | int/float | 大于 | `Field(..., gt=0)` |
|
|
| `lt` | int/float | 小于 | `Field(..., lt=100)` |
|
|
| `description` | any | 字段描述(Swagger 文档) | `Field(..., description="名称")` |
|
|
|
|
:::tip Field 第一个参数
|
|
- `...`:必填字段
|
|
- `None`:可选字段
|
|
- 具体值:默认值
|
|
:::
|
|
|
|
## field_validator 自定义校验
|
|
|
|
```python
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
|
|
class UserForm(BaseSchema):
|
|
"""用户创建/编辑表单"""
|
|
username: str = Field(..., min_length=3, max_length=50, description="用户名")
|
|
email: str = Field(..., max_length=100, description="邮箱")
|
|
phone: Optional[str] = Field(None, max_length=20, description="手机号")
|
|
|
|
@field_validator('username')
|
|
@classmethod
|
|
def validate_username(cls, v):
|
|
"""用户名只允许字母、数字、下划线"""
|
|
import re
|
|
if not re.match(r'^[a-zA-Z0-9_]+$', v):
|
|
raise ValueError('用户名只允许字母、数字、下划线')
|
|
return v
|
|
|
|
@field_validator('email')
|
|
@classmethod
|
|
def validate_email(cls, v):
|
|
"""邮箱格式校验"""
|
|
import re
|
|
if not re.match(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', v):
|
|
raise ValueError('邮箱格式不正确')
|
|
return v
|
|
|
|
@field_validator('phone')
|
|
@classmethod
|
|
def validate_phone(cls, v):
|
|
"""手机号格式校验(可选字段,有值时校验)"""
|
|
if v is not None and v != '':
|
|
import re
|
|
if not re.match(r'^1[3-9]\d{9}$', v):
|
|
raise ValueError('手机号格式不正确')
|
|
return v
|
|
```
|
|
|
|
:::tip validator 规则
|
|
- 使用 `@field_validator('field_name')` 装饰器(Pydantic v2 语法)
|
|
- 必须加 `@classmethod`
|
|
- 校验失败抛出 `ValueError(错误消息)`
|
|
- 可选字段需判断 `None` 值
|
|
:::
|
|
|
|
## 唯一性校验
|
|
|
|
唯一性校验通过 Service 层的 `unique_fields` 声明,而非在 Schema 中实现:
|
|
|
|
```python
|
|
class PositionService(BaseService):
|
|
model = Position
|
|
repo_cls = PositionRepository
|
|
unique_fields = {'name': '岗位名称不能重复'}
|
|
```
|
|
|
|
`BaseService._check_unique()` 在 `add()` 和 `update()` 时自动查询数据库判断是否重复,编辑时自动排除自身。
|
|
|
|
:::tip 唯一性校验位置
|
|
简单模块的唯一性校验可在 Schema 中完成。复杂模块(如用户、角色)的唯一性校验在 Service 层的 `_check_unique` 方法中统一处理,通过 `unique_fields` 字典声明。
|
|
:::
|
|
|
|
## 在 Handler 中使用
|
|
|
|
```python
|
|
class PositionAddHandler(BaseHandler):
|
|
@permission_required("sys:position:add")
|
|
@check_demo
|
|
async def post(self):
|
|
"""新增岗位"""
|
|
return await position_service.add(self)
|
|
```
|
|
|
|
:::tip 数据访问
|
|
Service 层通过 `data.field` 直接访问字段,或通过 `data.model_dump()` 获取完整字典。Pydantic 在进入 Endpoint 之前已完成校验,Service 无需重复验证。
|
|
:::
|
|
|
|
## 错误响应
|
|
|
|
Pydantic 校验失败时,由全局 `RequestValidationError` 处理器自动转换:
|
|
|
|
```json
|
|
{
|
|
"code": 1,
|
|
"data": null,
|
|
"msg": "name: 字段不能为空 / status: 输入值超出范围",
|
|
"ok": false
|
|
}
|
|
```
|
|
|
|
## 总结
|
|
|
|
Pydantic Schema 规范要求:表单类命名 `{Module}Form`,继承 `BaseSchema`;字段约束用 `Field(...)` 定义长度/范围;自定义校验用 `@field_validator` 抛 `ValueError`;唯一性校验在 Schema 或 Service 的 `unique_fields` 中完成。Tornado 自动解析和验证,Service 直接使用 `data.field` 访问已校验的数据。
|