优化更新
This commit is contained in:
@@ -51,7 +51,7 @@
|
||||
### R.ok() 成功响应
|
||||
|
||||
```python
|
||||
from src.core.response import R
|
||||
from src.core import response as R
|
||||
|
||||
# 基础成功响应
|
||||
return R.ok()
|
||||
@@ -114,7 +114,7 @@ return R.page(data=records, total=100, current=1, size=10)
|
||||
## 在 Router 中使用
|
||||
|
||||
```python
|
||||
from src.core.response import R
|
||||
from src.core import response as R
|
||||
|
||||
# 查询详情:service 返回 dict,router 包 R.ok
|
||||
@position_blue.route('/detail/<int:id>', methods=['GET'])
|
||||
|
||||
@@ -51,9 +51,9 @@ Content-Type: application/json
|
||||
@check_permission('sys:position:batchDelete')
|
||||
def batch_delete():
|
||||
"""批量删除岗位"""
|
||||
from src.modules.system.position.service import batch_delete_positions
|
||||
id_list = parse_id_list(request)
|
||||
return batch_delete_positions(id_list)
|
||||
from src.modules.system.position.service import delete_positions
|
||||
id_list = parse_batch_ids(request)
|
||||
return delete_positions(id_list)
|
||||
```
|
||||
|
||||
## 删除流程
|
||||
@@ -61,7 +61,7 @@ def batch_delete():
|
||||
```
|
||||
1. Router 接收请求
|
||||
↓
|
||||
2. parse_id_list(request) 解析 ID 数组
|
||||
2. parse_batch_ids(request) 解析 ID 数组
|
||||
↓
|
||||
3. Service.batch_delete(id_list)
|
||||
↓
|
||||
|
||||
@@ -136,7 +136,7 @@ wiki/ # VitePress 文档站
|
||||
│ ├── 3. 模块开发实战/ # volume2/(Model→Schema→Repo→Service→Router→前端)
|
||||
│ ├── 4. 架构设计/ # volume3/(分层、基类、缓存、安全)
|
||||
│ ├── 5. 开发指南/ # volume4/
|
||||
│ │ ├── 5.1 后端核心功能/ # core/(JWT、RBAC、字典、日志、限流、事务)
|
||||
│ │ ├── 5.1 后端核心功能/ # core/(JWT、RBAC、字典、日志、事务)
|
||||
│ │ ├── 5.2 后端业务模块/ # business/(用户、角色、菜单、文章、任务)
|
||||
│ │ ├── 5.3 后端通用工具/ # tools/(上传、Excel、IP/UA、密码、富文本)
|
||||
│ │ ├── 5.4 前端开发/ # frontend/(组件、路由、Store、构建)
|
||||
|
||||
@@ -96,9 +96,9 @@ def status():
|
||||
@position_blue.route('/batchDelete', methods=['DELETE'])
|
||||
@check_permission('sys:position:batchDelete')
|
||||
def batch_delete():
|
||||
from src.modules.system.position.service import batch_delete_positions
|
||||
id_list = parse_id_list(request)
|
||||
return batch_delete_positions(id_list)
|
||||
from src.modules.system.position.service import delete_positions
|
||||
id_list = parse_batch_ids(request)
|
||||
return delete_positions(id_list)
|
||||
```
|
||||
|
||||
## 权限标识规范
|
||||
@@ -126,8 +126,8 @@ def batch_delete():
|
||||
from flask import Blueprint, request
|
||||
|
||||
from src.core.permission import check_permission
|
||||
from src.core.response import R
|
||||
from src.common.utils.request import parse_request_body, parse_id_list
|
||||
from src.core import response as R
|
||||
from src.common.utils.request import parse_request_body, parse_batch_ids
|
||||
|
||||
# 创建 Blueprint 实例
|
||||
position_blue = Blueprint('position', __name__, url_prefix='/position')
|
||||
@@ -191,9 +191,9 @@ def status():
|
||||
@position_blue.route('/batchDelete', methods=['DELETE'])
|
||||
@check_permission('sys:position:batchDelete')
|
||||
def batch_delete():
|
||||
from src.modules.system.position.service import batch_delete_positions
|
||||
id_list = parse_id_list(request)
|
||||
return batch_delete_positions(id_list)
|
||||
from src.modules.system.position.service import delete_positions
|
||||
id_list = parse_batch_ids(request)
|
||||
return delete_positions(id_list)
|
||||
```
|
||||
|
||||
## 路由注册
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
## 概述
|
||||
|
||||
前端基于 Vue3 + ElementPlus + 自定义组件库构建。页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套,分页由 `BasicTable` 内置处理,无需单独引入分页组件。
|
||||
前端基于 Vue3 + AntDesign Vue + 自定义组件库构建。页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `bodyCell` 插槽四件套,分页由 `BasicTable` 内置处理,无需单独引入分页组件。
|
||||
|
||||
:::tip 技术栈
|
||||
- Vue3(Composition API + `<script setup>`)
|
||||
- ElementPlus 2.x
|
||||
- 自定义组件(BasicTable / BasicForm / TableAction / PageWrapper)
|
||||
- AntDesign Vue 4.x
|
||||
- 自定义组件(BasicTable / BasicForm / PageWrapper)
|
||||
- Pinia 状态管理
|
||||
- Vue Router 路由管理
|
||||
:::
|
||||
@@ -56,7 +56,7 @@ ui/src/
|
||||
├── utils/ # 工具函数
|
||||
├── hooks/ # 组合式函数
|
||||
├── styles/ # 全局样式
|
||||
├── plugins/ # 插件注册(ElementPlus、自定义组件、指令)
|
||||
├── plugins/ # 插件注册(AntDesign Vue、自定义组件、指令)
|
||||
└── main.ts # 入口文件
|
||||
```
|
||||
|
||||
@@ -79,37 +79,42 @@ ui/src/
|
||||
<template>
|
||||
<PageWrapper>
|
||||
<!-- 搜索表单 -->
|
||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<a-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||||
</el-card>
|
||||
</a-card>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-card :bordered="false" class="proCard">
|
||||
<a-card :bordered="false" class="proCard">
|
||||
<BasicTable
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
:row-key="(row) => row.id"
|
||||
ref="tableRef"
|
||||
:actionColumn="actionColumn"
|
||||
@selection-change="onSelectionChange"
|
||||
scroll-x="1200"
|
||||
:row-selection="{ onChange: onSelectionChange }"
|
||||
>
|
||||
<template #tableTitle>
|
||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:param:add']">
|
||||
<el-icon><PlusOutlined /></el-icon>
|
||||
添加参数
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="handleDelete()"
|
||||
:disabled="!selectionData.length"
|
||||
v-perm="['sys:param:batchDelete']"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd" v-perm="['sys:param:add']">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建
|
||||
</a-button>
|
||||
<a-button type="danger" @click="handleDelete()" :disabled="!selectionData.length" v-perm="['sys:param:batchDelete']">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleEdit(record.id)" v-perm="['sys:param:update']">编辑</a-button>
|
||||
<a-button type="primary" danger @click="handleDelete(record.id)" v-perm="['sys:param:delete']">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</el-card>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗(懒加载) -->
|
||||
<editDialog
|
||||
@@ -133,56 +138,59 @@ ui/src/
|
||||
```typescript
|
||||
// views/data/param/columns.ts
|
||||
import { h } from 'vue';
|
||||
import { ElTag } from 'element-plus';
|
||||
import { Tag } from 'ant-design-vue';
|
||||
|
||||
export const columns = [
|
||||
{ type: 'selection' }, // 多选列
|
||||
{ label: 'ID', prop: 'id', width: 50 }, // 固定列
|
||||
{ label: '参数名称', prop: 'name', minWidth: 200 },
|
||||
{ label: '参数编码', prop: 'code', minWidth: 250 },
|
||||
{ label: '参数值', prop: 'value', minWidth: 200 },
|
||||
{ title: 'ID', dataIndex: 'id', width: 50, fixed: 'left' },
|
||||
{ title: '参数名称', dataIndex: 'name', width: 200 },
|
||||
{ title: '参数编码', dataIndex: 'code', width: 250 },
|
||||
{ title: '参数值', dataIndex: 'value', width: 200 },
|
||||
{
|
||||
label: '参数类型',
|
||||
prop: 'type',
|
||||
minWidth: 100,
|
||||
render(record) {
|
||||
return h(ElTag,
|
||||
{ type: record.row.type == 0 ? 'primary' : 'warning' },
|
||||
{ default: () => (record.row.type == 0 ? '系统' : '业务') }
|
||||
title: '参数类型',
|
||||
dataIndex: 'type',
|
||||
width: 100,
|
||||
customRender({ record }) {
|
||||
return h(Tag,
|
||||
{ color: record.type == 0 ? 'processing' : 'warning' },
|
||||
{ default: () => (record.type == 0 ? '系统' : '业务') }
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
minWidth: 100,
|
||||
render(record) {
|
||||
return h(ElTag,
|
||||
{ type: record.row.status == 1 ? 'success' : 'danger' },
|
||||
{ default: () => (record.row.status == 1 ? '正常' : '禁用') }
|
||||
title: '参数状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
customRender({ record }) {
|
||||
return h(Tag,
|
||||
{ color: record.status == 1 ? 'success' : 'error' },
|
||||
{ default: () => (record.status == 1 ? '正常' : '禁用') }
|
||||
);
|
||||
},
|
||||
},
|
||||
{ label: '创建时间', prop: 'createTime', width: 180 },
|
||||
{ title: '排序', dataIndex: 'sort', width: 100 },
|
||||
{ title: '备注', dataIndex: 'note', width: 200 },
|
||||
{ title: '创建人', dataIndex: 'createUser', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
|
||||
{ title: '操作', fixed: 'right', dataIndex: 'action', key: 'action', width: 200 },
|
||||
];
|
||||
```
|
||||
|
||||
### render 函数自定义渲染
|
||||
### customRender 函数自定义渲染
|
||||
|
||||
```typescript
|
||||
// 状态标签
|
||||
render(record) {
|
||||
return h(ElTag,
|
||||
{ type: record.row.status == 1 ? 'success' : 'danger' },
|
||||
{ default: () => record.row.statusText } // 使用后端 serialize_maps 返回的 xxxText
|
||||
customRender({ record }) {
|
||||
return h(Tag,
|
||||
{ color: record.status == 1 ? 'success' : 'error' },
|
||||
{ default: () => record.statusText }
|
||||
);
|
||||
}
|
||||
|
||||
// 链接
|
||||
render(record) {
|
||||
customRender({ record }) {
|
||||
return h('a',
|
||||
{ href: record.row.url, target: '_blank', class: 'text-blue-500' },
|
||||
{ default: () => record.row.url }
|
||||
{ href: record.url, target: '_blank', class: 'text-blue-500' },
|
||||
{ default: () => record.url }
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -195,13 +203,13 @@ import { FormSchema } from '@/components/Form/index';
|
||||
|
||||
export const schemas: FormSchema[] = [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'name',
|
||||
component: 'Input',
|
||||
label: '参数名称',
|
||||
componentProps: { placeholder: '请输入参数名称' },
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
name: 'type',
|
||||
component: 'Select',
|
||||
label: '参数类型',
|
||||
componentProps: {
|
||||
@@ -214,7 +222,7 @@ export const schemas: FormSchema[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
name: 'status',
|
||||
component: 'Select',
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
@@ -229,38 +237,6 @@ export const schemas: FormSchema[] = [
|
||||
];
|
||||
```
|
||||
|
||||
## 操作栏(TableAction)
|
||||
|
||||
```typescript
|
||||
const actionColumn = reactive({
|
||||
width: 200,
|
||||
label: '操作',
|
||||
prop: 'action',
|
||||
fixed: 'right',
|
||||
render(record) {
|
||||
return h(TableAction, {
|
||||
style: 'button',
|
||||
actions: [
|
||||
{
|
||||
label: '编辑',
|
||||
icon: 'Edit',
|
||||
type: 'warning',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
auth: ['sys:param:update'],
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
icon: 'Delete',
|
||||
type: 'danger',
|
||||
onClick: handleDelete.bind(null, record),
|
||||
auth: ['sys:param:delete'],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## API 接口定义
|
||||
|
||||
```typescript
|
||||
@@ -298,7 +274,7 @@ export function paramBatchDelete(data: any) {
|
||||
// 加载数据(BasicTable 的 :request 回调,自动传入分页参数)
|
||||
const loadDataTable = async (res: any) => {
|
||||
const result = await getParamList({ ...formParams, ...res });
|
||||
return result; // 返回 { items, total } 或 R.ok(data=列表, count=总数)
|
||||
return result;
|
||||
};
|
||||
|
||||
// 刷新表格
|
||||
@@ -308,11 +284,13 @@ function reloadTable(noRefresh = '') {
|
||||
|
||||
// 搜索提交
|
||||
function handleSubmit(values: Recordable) {
|
||||
handleReset();
|
||||
for (const key in formParams) {
|
||||
formParams[key] = '';
|
||||
}
|
||||
for (const key in values) {
|
||||
formParams[key] = values[key];
|
||||
}
|
||||
reloadTable(); // 重置到第一页
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
@@ -320,6 +298,7 @@ function handleReset() {
|
||||
for (const key in formParams) {
|
||||
formParams[key] = '';
|
||||
}
|
||||
reloadTable();
|
||||
}
|
||||
```
|
||||
|
||||
@@ -349,26 +328,28 @@ const handleAdd = async () => {
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = async (record: Recordable) => {
|
||||
paramId.value = record.row.id;
|
||||
async function handleEdit(id) {
|
||||
paramId.value = id;
|
||||
await nextTick();
|
||||
editVisible.value = true;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 删除(单条 + 批量)
|
||||
|
||||
```typescript
|
||||
async function handleDelete(record: Recordable) {
|
||||
let ids = [];
|
||||
if (!record) {
|
||||
// 批量删除:取多选行的 ID
|
||||
ids = selectionData.value.map(({ id }) => id);
|
||||
}
|
||||
await confirm('确定要删除?');
|
||||
record ? await paramDelete(record.row.id) : await paramBatchDelete(ids);
|
||||
message('删除成功');
|
||||
reloadTable();
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
|
||||
async function handleDelete(id) {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除?',
|
||||
onOk: async () => {
|
||||
id ? await paramDelete(id) : await paramBatchDelete(selectionData.value);
|
||||
message.success('删除成功');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 多选变化
|
||||
@@ -388,4 +369,4 @@ function onSelectionChange(value) {
|
||||
|
||||
## 总结
|
||||
|
||||
前端页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套。分页由 `BasicTable` 内置处理,无需单独引入分页组件。每个模块包含 `index.vue`(列表页)、`edit.vue`(编辑弹窗)、`columns.ts`(列定义)、`querySchemas.ts`(搜索配置)四个文件。API 接口按模块分文件定义在 `api/` 目录下。
|
||||
前端页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `bodyCell` 插槽四件套。分页由 `BasicTable` 内置处理,无需单独引入分页组件。每个模块包含 `index.vue`(列表页)、`edit.vue`(编辑弹窗)、`columns.ts`(列定义)、`querySchemas.ts`(搜索配置)四个文件。API 接口按模块分文件定义在 `api/` 目录下。
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
# 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']` 访问已校验的数据。
|
||||
@@ -2,134 +2,234 @@
|
||||
|
||||
## 概述
|
||||
|
||||
Router 层是 HTTP 接口层,负责定义路由、权限装饰器和请求参数绑定。每个业务模块对应一个 router 文件,所有业务逻辑委托给 Service 层处理。
|
||||
Router 层是 HTTP 接口层,负责定义路由、权限装饰器和请求参数解析。每个业务模块对应一个 router 文件,所有业务逻辑委托给 Service 层处理。
|
||||
|
||||
:::tip 设计原则
|
||||
- Router 层不含业务逻辑,仅做路由定义、权限校验和参数绑定
|
||||
- 装饰器顺序固定为:路由 → 权限
|
||||
- Service 函数使用延迟导入,避免循环引用
|
||||
- Router 层不含业务逻辑,仅做路由定义、权限校验和参数解析
|
||||
- 装饰器顺序固定为:路由 → 权限 → 演示模式 → 操作日志
|
||||
- Service 模块在文件顶部统一导入(`from src.modules.system.position import service as position`)
|
||||
- Router 负责调用 `R.ok()` / `R.failed()` 封装响应,Service 不返回 R 对象
|
||||
:::
|
||||
|
||||
## 完整示例
|
||||
|
||||
以岗位模块为例,`src/modules/system/position/router.py` 完整内容:
|
||||
以岗位模块为例,以下为 `src/modules/system/position/router.py` 的核心代码(省略版权头):
|
||||
|
||||
```python
|
||||
"""
|
||||
岗位管理控制器模块
|
||||
=============================================================
|
||||
|
||||
提供岗位(Position)相关的API接口,包括:
|
||||
- 岗位分页查询
|
||||
- 岗位详情查询
|
||||
- 岗位添加、更新、删除
|
||||
- 岗位状态设置
|
||||
- 岗位数据列表获取
|
||||
"""
|
||||
from flask import Blueprint, request
|
||||
|
||||
from src.middleware import check_demo, operation_log
|
||||
from src.core.permission import check_permission
|
||||
from src.core.response import R
|
||||
from src.common.utils.request import parse_request_body, parse_id_list
|
||||
from src.modules.system.position import service as position
|
||||
from src.common.utils.request import parse_batch_ids, parse_request_body
|
||||
|
||||
from src.core import response as R
|
||||
|
||||
# ============================================================
|
||||
# 创建 Blueprint 实例
|
||||
# ============================================================
|
||||
position_blue = Blueprint('position', __name__, url_prefix='/position')
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 查询分页数据
|
||||
# 岗位分页查询
|
||||
# ============================================================
|
||||
@position_blue.route('/page', methods=['GET'])
|
||||
@check_permission('sys:position:page')
|
||||
|
||||
@position_blue.route('/page', methods=["GET"])
|
||||
@check_permission("sys:position:page")
|
||||
@operation_log("岗位管理", "查询")
|
||||
def page():
|
||||
"""获取岗位分页列表"""
|
||||
from src.modules.system.position.service import get_position_page
|
||||
return get_position_page()
|
||||
"""查询岗位分页数据"""
|
||||
return R.ok(data=position.get_position_page(request.args))
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 查询岗位详情
|
||||
# 岗位详情查询
|
||||
# ============================================================
|
||||
@position_blue.route('/detail/<int:id>', methods=['GET'])
|
||||
@check_permission('sys:position:detail')
|
||||
|
||||
@position_blue.route('/detail/<int:id>', methods=["GET"])
|
||||
@check_permission("sys:position:detail")
|
||||
@operation_log("岗位管理", "详情")
|
||||
def detail(id):
|
||||
"""根据岗位ID查询详细信息"""
|
||||
from src.modules.system.position.service import get_position_detail
|
||||
return R.ok(data=get_position_detail(id))
|
||||
"""查询岗位详情"""
|
||||
data = position.get_position_detail(id)
|
||||
return R.ok(data=data)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 获取岗位列表
|
||||
# 岗位添加
|
||||
# ============================================================
|
||||
@position_blue.route('/list', methods=['GET'])
|
||||
@check_permission('sys:position:list')
|
||||
def get_position_list():
|
||||
"""获取所有岗位列表(无分页,用于下拉选择)"""
|
||||
from src.modules.system.position.service import get_position_list
|
||||
return R.ok(data=get_position_list())
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 添加岗位
|
||||
# ============================================================
|
||||
@position_blue.route('/add', methods=['POST'])
|
||||
@check_permission('sys:position:add')
|
||||
@position_blue.route('/add', methods=["POST"])
|
||||
@check_permission("sys:position:add")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "新增")
|
||||
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)
|
||||
"""添加岗位"""
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
position.add_position(json_data)
|
||||
return R.ok(msg="添加成功")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 更新岗位
|
||||
# 岗位更新
|
||||
# ============================================================
|
||||
@position_blue.route('/update', methods=['PUT'])
|
||||
@check_permission('sys:position:update')
|
||||
|
||||
@position_blue.route('/update', methods=["PUT"])
|
||||
@check_permission("sys:position:update")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "修改")
|
||||
def update():
|
||||
"""更新岗位信息"""
|
||||
from src.modules.system.position.service import update_position
|
||||
from src.modules.system.position.schema import PositionForm
|
||||
data = parse_request_body(request, PositionForm)
|
||||
return update_position(data)
|
||||
"""更新岗位"""
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
position.update_position(json_data)
|
||||
return R.ok(msg="更新成功")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 删除岗位
|
||||
# 岗位删除
|
||||
# ============================================================
|
||||
@position_blue.route('/delete/<int:id>', methods=['DELETE'])
|
||||
@check_permission('sys:position:delete')
|
||||
|
||||
@position_blue.route('/delete/<string:id>', methods=["DELETE"])
|
||||
@check_permission("sys:position:delete")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "删除")
|
||||
def delete(id):
|
||||
"""根据ID删除单个岗位"""
|
||||
from src.modules.system.position.service import delete_position
|
||||
return delete_position(id)
|
||||
"""删除岗位(支持逗号分隔的多个ID)"""
|
||||
count = position.delete_positions(id)
|
||||
return R.ok(msg=f"本次共删除{count}条数据")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 设置岗位状态
|
||||
# 岗位批量删除
|
||||
# ============================================================
|
||||
@position_blue.route('/status', methods=['PUT'])
|
||||
@check_permission('sys:position:status')
|
||||
def status():
|
||||
"""修改岗位状态(启用/禁用)"""
|
||||
from src.modules.system.position.service import update_position_status
|
||||
from src.modules.system.position.schema import PositionStatusForm
|
||||
data = parse_request_body(request, PositionStatusForm)
|
||||
return update_position_status(data)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 批量删除岗位
|
||||
# ============================================================
|
||||
@position_blue.route('/batchDelete', methods=['DELETE'])
|
||||
@check_permission('sys:position:batchDelete')
|
||||
@position_blue.route('/batchDelete', methods=["DELETE"])
|
||||
@check_permission("sys:position:batchDelete")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "批量删除")
|
||||
def batch_delete():
|
||||
"""批量删除岗位"""
|
||||
from src.modules.system.position.service import batch_delete_positions
|
||||
id_list = parse_id_list(request)
|
||||
return batch_delete_positions(id_list)
|
||||
id_str, err = parse_batch_ids()
|
||||
if err:
|
||||
return err
|
||||
count = position.delete_positions(id_str)
|
||||
return R.ok(msg=f"本次共删除{count}条数据")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位状态设置
|
||||
# ============================================================
|
||||
|
||||
@position_blue.route('/status', methods=["PUT"])
|
||||
@check_permission("sys:position:status")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "状态变更")
|
||||
def status():
|
||||
"""设置岗位状态"""
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
position.update_position_status(json_data)
|
||||
return R.ok(msg="设置成功")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位列表获取(无分页)
|
||||
# ============================================================
|
||||
|
||||
@position_blue.route('/list', methods=["GET"])
|
||||
@check_permission("sys:position:list")
|
||||
@operation_log("岗位管理", "查询")
|
||||
def get_position_list():
|
||||
"""获取岗位数据列表(无分页,用于下拉选择)"""
|
||||
result = position.get_position_list(request.args)
|
||||
return R.ok(data=result)
|
||||
```
|
||||
|
||||
## 装饰器说明
|
||||
|
||||
| 装饰器 | 作用 | 位置 |
|
||||
| 装饰器 | 作用 | 适用场景 |
|
||||
|:----|:----|:----|
|
||||
| `@position_blue.route()` | 定义 HTTP 方法和路径 | 最外层 |
|
||||
| `@check_permission('sys:position:page')` | RBAC 权限校验 | 路由装饰器之后 |
|
||||
| `@position_blue.route()` | 定义 HTTP 方法和路径 | 所有接口 |
|
||||
| `@check_permission('sys:position:page')` | RBAC 权限校验 | 所有接口 |
|
||||
| `@check_demo` | 演示模式拦截(禁止写操作) | 写操作(add/update/delete/status/batchDelete) |
|
||||
| `@operation_log("岗位管理", "查询")` | 操作日志自动记录 | 所有接口 |
|
||||
|
||||
**装饰器顺序必须严格遵守**:路由 → 权限。顺序错误会导致功能异常。
|
||||
**装饰器顺序必须严格遵守**:路由 → 权限 → 演示模式 → 操作日志。
|
||||
|
||||
## 导入规范
|
||||
|
||||
```python
|
||||
# 模块顶部统一导入
|
||||
from src.middleware import check_demo, operation_log
|
||||
from src.core.permission import check_permission
|
||||
from src.modules.system.position import service as position
|
||||
from src.common.utils.request import parse_batch_ids, parse_request_body
|
||||
from src.core import response as R
|
||||
```
|
||||
|
||||
| 导入项 | 来源 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| `check_demo` | `src.middleware` | 演示模式装饰器 |
|
||||
| `operation_log` | `src.middleware` | 操作日志装饰器 |
|
||||
| `check_permission` | `src.core.permission` | 权限校验装饰器 |
|
||||
| `service as position` | 业务模块 | Service 层代理函数 |
|
||||
| `parse_request_body` | `src.common.utils.request` | 解析请求体 JSON |
|
||||
| `parse_batch_ids` | `src.common.utils.request` | 解析批量删除的 ID 数组 |
|
||||
| `R` | `src.core` | 统一响应封装 |
|
||||
|
||||
## 请求参数解析
|
||||
|
||||
```python
|
||||
# 解析请求体(返回元组:数据, 错误信息)
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
|
||||
# 解析批量删除的ID数组(返回元组:ID字符串, 错误信息)
|
||||
id_str, err = parse_batch_ids()
|
||||
if err:
|
||||
return err
|
||||
```
|
||||
|
||||
| 函数 | 作用 | 返回值 |
|
||||
|:----|:----|:----|
|
||||
| `parse_request_body()` | 从请求体解析 JSON | `(dict, None)` 或 `(None, error_msg)` |
|
||||
| `parse_batch_ids()` | 从请求体解析 ID 数组 | `(id_str, None)` 或 `(None, R.failed(...))` |
|
||||
|
||||
## 响应封装
|
||||
|
||||
Router 层负责调用 `R.ok()` / `R.failed()` 封装响应:
|
||||
|
||||
```python
|
||||
# 查询类:Service 返回数据,Router 用 R.ok(data=...) 包装
|
||||
return R.ok(data=position.get_position_page(request.args))
|
||||
|
||||
# 写操作类:Service 执行成功后无返回值,Router 返回成功消息
|
||||
position.add_position(json_data)
|
||||
return R.ok(msg="添加成功")
|
||||
|
||||
# 删除类:Service 返回删除数量,Router 格式化消息
|
||||
count = position.delete_positions(id)
|
||||
return R.ok(msg=f"本次共删除{count}条数据")
|
||||
```
|
||||
|
||||
:::warning Service 不返回 R 对象
|
||||
Service 层入参为数据字典、出参为纯数据(或 None),业务失败通过 `raise BizError` 表达。Router 层负责封装 `R.ok()` / `R.failed()` 响应。
|
||||
:::
|
||||
|
||||
## 权限字符串命名规范
|
||||
|
||||
@@ -148,28 +248,6 @@ sys:{module}:{action}
|
||||
| `batchDelete` | DELETE | 批量删除 |
|
||||
| `status` | PUT | 状态变更 |
|
||||
|
||||
## 请求参数解析
|
||||
|
||||
```python
|
||||
from src.common.utils.request import parse_request_body, parse_id_list
|
||||
|
||||
# 解析请求体
|
||||
data = parse_request_body(request, PositionForm)
|
||||
|
||||
# 解析ID列表
|
||||
id_list = parse_id_list(request)
|
||||
```
|
||||
|
||||
| 函数 | 作用 |
|
||||
|:----|:----|
|
||||
| `parse_request_body(request, FormClass)` | 从请求体解析 JSON 并校验 |
|
||||
| `parse_id_list(request)` | 从请求体解析 ID 数组 |
|
||||
|
||||
## 响应封装
|
||||
|
||||
- **Service 返回 R 对象的**(如 `add_position`、`update_position`):直接 `return`
|
||||
- **Service 返回原始数据的**(如 `get_position_detail`):用 `R.ok(data=...)` 包装
|
||||
|
||||
## 模块注册
|
||||
|
||||
在 `src/routes/v1.py` 中注册模块:
|
||||
@@ -186,13 +264,13 @@ BLUEPRINTS = [
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **Router 层不含业务逻辑**:仅做路由定义、权限校验和参数绑定
|
||||
2. **装饰器顺序**:路由 → 权限
|
||||
3. **使用 `parse_request_body` 解析请求体**:自动执行 WTForms 校验
|
||||
4. **使用 `parse_id_list` 解析批量操作的 ID 数组**
|
||||
5. **Service 函数使用延迟导入**:避免循环引用
|
||||
6. **权限字符串与代码完全一致**:确保权限校验正确
|
||||
1. **Router 层不含业务逻辑**:仅做路由定义、权限校验和参数解析
|
||||
2. **装饰器顺序**:路由 → 权限 → 演示模式 → 操作日志
|
||||
3. **Service 模块顶部统一导入**:`from src.modules.system.position import service as position`
|
||||
4. **使用 `parse_request_body()` 解析请求体**:返回 `(data, err)` 元组
|
||||
5. **使用 `parse_batch_ids()` 解析批量操作的 ID 数组**
|
||||
6. **Router 负责封装响应**:Service 不返回 R 对象
|
||||
|
||||
## 总结
|
||||
|
||||
Router 层是 HTTP 请求的入口,负责路由定义、权限校验和参数绑定,不含业务逻辑。装饰器顺序固定为路由 → 权限,使用 `parse_request_body` 解析请求体,使用 `parse_id_list` 解析批量操作的 ID 数组。Service 函数使用延迟导入,避免循环引用。
|
||||
Router 层是 HTTP 请求的入口,负责路由定义、权限校验和参数解析,不含业务逻辑。装饰器顺序固定为路由 → 权限 → 演示模式 → 操作日志。Service 模块在文件顶部统一导入,Router 负责调用 `R.ok()` / `R.failed()` 封装响应,Service 不返回 R 对象。
|
||||
|
||||
Reference in New Issue
Block a user