191 lines
6.5 KiB
Markdown
191 lines
6.5 KiB
Markdown
# WTForms Schema规范
|
|
|
|
## 概述
|
|
|
|
项目使用 WTForms 进行请求数据校验。所有请求体参数通过 WTForms 表单类定义,框架在调用 Router 前自动完成解析和验证。
|
|
|
|
:::tip 核心原则
|
|
- 请求体参数必须声明为 WTForms 表单类(Form 类)
|
|
- 字段约束使用 `validators` 定义
|
|
- 自定义校验使用 `validate_` 方法
|
|
- 唯一性校验在 Schema 中查询数据库
|
|
:::
|
|
|
|
## 命名规范
|
|
|
|
| 类型 | 命名格式 | 示例 |
|
|
|------|---------|------|
|
|
| 创建/编辑表单 | `{模块}Form` | `PositionForm` |
|
|
| 状态更新表单 | `{模块}StatusForm` | `PositionStatusForm` |
|
|
| 查询参数表单 | `{模块}QueryForm` | `PositionQueryForm` |
|
|
|
|
:::tip 继承 BaseForm
|
|
创建/编辑表单继承 `src.common.forms.base_form.BaseForm`,自动获得 `id` 字段(编辑时必填)。
|
|
:::
|
|
|
|
## PositionForm 示例
|
|
|
|
以岗位模块为例,`src/modules/system/position/schema.py` 完整内容:
|
|
|
|
```python
|
|
from wtforms import StringField, IntegerField
|
|
from wtforms.validators import DataRequired, Length, NumberRange
|
|
|
|
from src.common.forms.base_form import BaseForm
|
|
|
|
|
|
class PositionForm(BaseForm):
|
|
"""岗位创建/编辑表单"""
|
|
name = StringField('岗位名称', validators=[
|
|
DataRequired(message='岗位名称不能为空'),
|
|
Length(min=1, max=150, message='岗位名称长度必须在1-150个字符之间')
|
|
])
|
|
status = IntegerField('岗位状态', validators=[
|
|
DataRequired(message='岗位状态不能为空'),
|
|
NumberRange(min=1, max=2, message='岗位状态必须是1或2')
|
|
])
|
|
sort = IntegerField('岗位排序', validators=[
|
|
DataRequired(message='岗位排序不能为空'),
|
|
NumberRange(min=0, max=99999, message='岗位排序必须在0-99999之间')
|
|
])
|
|
|
|
|
|
class PositionStatusForm(BaseForm):
|
|
"""岗位状态更新表单"""
|
|
id = IntegerField('岗位ID', validators=[
|
|
DataRequired(message='岗位ID不能为空'),
|
|
NumberRange(min=1, message='岗位ID必须大于0')
|
|
])
|
|
status = IntegerField('岗位状态', validators=[
|
|
DataRequired(message='岗位状态不能为空'),
|
|
NumberRange(min=1, max=2, message='岗位状态必须是1或2')
|
|
])
|
|
```
|
|
|
|
:::tip BaseForm 基类
|
|
`BaseForm`(定义在 `src/common/forms/base_form.py`)提供 `id` 字段和数据清洗功能,自动将前端传入的空字符串 `''`/`'null'`/`'undefined'` 转为 `None`,避免 WTForms 解析报错。
|
|
:::
|
|
|
|
## Validator 约束
|
|
|
|
| Validator | 类型 | 说明 | 示例 |
|
|
|------|------|------|------|
|
|
| `DataRequired` | any | 必填字段 | `DataRequired(message='不能为空')` |
|
|
| `Length` | str | 字符串长度 | `Length(min=1, max=150)` |
|
|
| `NumberRange` | int/float | 数值范围 | `NumberRange(min=0, max=99999)` |
|
|
| `Optional` | any | 可选字段 | `Optional()` |
|
|
| `Email` | str | 邮箱格式 | `Email(message='邮箱格式不正确')` |
|
|
| `URL` | str | URL 格式 | `URL(message='URL格式不正确')` |
|
|
| `Regexp` | str | 正则表达式 | `Regexp(r'^[a-zA-Z]+$')` |
|
|
|
|
:::tip Validator 说明
|
|
- `DataRequired`:必填字段,不允许空值
|
|
- `Length`:字符串长度限制
|
|
- `NumberRange`:数值范围限制
|
|
- `Optional`:可选字段
|
|
:::
|
|
|
|
## 自定义校验器
|
|
|
|
### 唯一性校验
|
|
|
|
```python
|
|
from wtforms import ValidationError
|
|
from src.modules.system.position.repository import PositionRepository
|
|
|
|
|
|
class PositionForm(BaseForm):
|
|
"""岗位创建/编辑表单"""
|
|
name = StringField('岗位名称', validators=[
|
|
DataRequired(message='岗位名称不能为空'),
|
|
Length(min=1, max=150, message='岗位名称长度必须在1-150个字符之间')
|
|
])
|
|
|
|
def validate_name(self, field):
|
|
"""岗位名称唯一性校验"""
|
|
exclude_id = self.id.data if self.id.data else None
|
|
if PositionRepository.exists_by([Position.name == field.data], exclude_id):
|
|
raise ValidationError('岗位名称不能重复')
|
|
```
|
|
|
|
### 格式校验
|
|
|
|
```python
|
|
from wtforms import ValidationError
|
|
import re
|
|
|
|
|
|
class UserForm(BaseForm):
|
|
"""用户创建/编辑表单"""
|
|
username = StringField('用户名', validators=[
|
|
DataRequired(message='用户名不能为空'),
|
|
Length(min=3, max=50, message='用户名长度必须在3-50个字符之间')
|
|
])
|
|
email = StringField('邮箱', validators=[
|
|
DataRequired(message='邮箱不能为空'),
|
|
Length(max=100, message='邮箱长度不能超过100个字符')
|
|
])
|
|
phone = StringField('手机号', validators=[
|
|
Optional(),
|
|
Length(max=20, message='手机号长度不能超过20个字符')
|
|
])
|
|
|
|
def validate_username(self, field):
|
|
"""用户名只允许字母、数字、下划线"""
|
|
if not re.match(r'^[a-zA-Z0-9_]+$', field.data):
|
|
raise ValidationError('用户名只允许字母、数字、下划线')
|
|
|
|
def validate_email(self, field):
|
|
"""邮箱格式校验"""
|
|
if not re.match(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', field.data):
|
|
raise ValidationError('邮箱格式不正确')
|
|
|
|
def validate_phone(self, field):
|
|
"""手机号格式校验(可选字段,有值时校验)"""
|
|
if field.data and field.data != '':
|
|
if not re.match(r'^1[3-9]\d{9}$', field.data):
|
|
raise ValidationError('手机号格式不正确')
|
|
```
|
|
|
|
:::tip validator 规则
|
|
- 方法名必须为 `validate_{field_name}`
|
|
- 校验失败抛出 `ValidationError(错误消息)`
|
|
- 可选字段需判断 `None` 值
|
|
:::
|
|
|
|
## 在 Router 中使用
|
|
|
|
```python
|
|
@position_blue.route('/add', methods=['POST'])
|
|
@check_permission('sys:position:add')
|
|
def add():
|
|
"""
|
|
新增岗位
|
|
"""
|
|
from src.modules.system.position.service import add_position
|
|
from src.modules.system.position.schema import PositionForm
|
|
data = parse_request_body(request, PositionForm)
|
|
return add_position(data)
|
|
```
|
|
|
|
:::tip 数据访问
|
|
Service 层通过 `data['field']` 直接访问字段。WTForms 在进入 Router 之前已完成校验,Service 无需重复验证。
|
|
:::
|
|
|
|
## 错误响应
|
|
|
|
WTForms 校验失败时,由框架自动转换:
|
|
|
|
```json
|
|
{
|
|
"code": 1,
|
|
"data": null,
|
|
"msg": "岗位名称不能为空",
|
|
"ok": false
|
|
}
|
|
```
|
|
|
|
## 总结
|
|
|
|
WTForms Schema 规范要求:表单类命名 `{Module}Form`,继承 `BaseForm`;字段约束用 `validators` 定义长度/范围;自定义校验用 `validate_{field_name}` 方法抛 `ValidationError`;唯一性校验通过 `Repository.exists_by(filters, exclude_id)` 实现。框架自动解析和验证,Service 通过 `form.data['field']` 访问已校验的数据。
|