Files
djangoadmin-wiki/DjangoAdmin_Flask_EleVue/zh/volume3/layer-arch.md
T
2026-09-13 20:29:19 +08:00

515 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 分层架构设计
本章详细描述项目的三层架构设计,包括各层的职责边界、依赖方向、通信方式,并以岗位(Position)模块为实例贯穿说明。
## 三层架构概览
:::tip 架构分层原则
采用经典的三层架构,核心原则是:**依赖单向、职责单一、接口隔离**。上层只依赖下层接口,下层不感知上层存在。
:::
```
┌─────────────────────────────────────────────────────────────┐
│ HTTP 层 (Router) │
│ modules/system/position/router.py │
│ 职责:路由定义 · 权限装饰器 · 参数接收 · 响应返回 │
│ 原则:不含业务逻辑,仅做请求转发 │
└──────────────────────────┬──────────────────────────────────┘
│ 调用
▼
┌─────────────────────────────────────────────────────────────┐
│ 业务层 (Service) │
│ modules/system/position/service.py │
│ 职责:业务逻辑 · 校验规则 · 字段组装 · 序列化加工 │
│ 原则:不直接操作数据库,通过 Repository 访问数据 │
└──────────────────────────┬──────────────────────────────────┘
│ 调用
▼
┌─────────────────────────────────────────────────────────────┐
│ 数据层 (Repository) │
│ modules/system/position/repository.py │
│ 职责:数据库 CRUD · 查询条件组装 · 软删除过滤 │
│ 原则:只做数据访问原语,不做业务校验和响应封装 │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 数据库 (MySQL / PostgreSQL / etc.) │
└─────────────────────────────────────────────────────────────┘
```
## 各层详细职责
### 第一层:HTTP 层(Router)
**目录位置**:`src/modules/{group}/{name}/router.py`
**核心职责**:
1. **路由定义**:声明 HTTP 方法、URL 路径、请求参数类型
2. **权限控制**:通过 `@check_permission` 装饰器声明所需权限
3. **参数接收**:通过 `parse_request_body` 函数解析请求体
4. **响应返回**:直接返回 Service 层的 `R` 对象
**设计原则**:
- 不包含任何业务逻辑
- 不直接访问数据库
- 仅做请求转发和响应返回
- 权限字符串遵循 `sys:{module}:{action}` 格式
**代码示例**(岗位模块):
```python
# src/modules/system/position/router.py
# +======================================================================
# | 模块: 岗位路由层
# | 说明: 提供岗位信息的增删改查、状态管理、列表获取等 API 接口
# +======================================================================
from flask import Blueprint, request
from src.core.permission import check_permission
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')
# ============================================================
# 查询分页数据
# ============================================================
@position_blue.route('/page', methods=['GET'])
@check_permission('sys:position:page')
def page():
"""
获取岗位分页列表
:return: 分页数据响应
"""
from src.modules.system.position.service import get_position_page
return get_position_page()
# ============================================================
# 查询岗位详情
# ============================================================
@position_blue.route('/detail/<int:id>', methods=['GET'])
@check_permission('sys:position:detail')
def detail(id):
"""
根据岗位ID查询详细信息
:param id: 岗位ID(路径参数)
:return: 岗位详情数据
"""
from src.modules.system.position.service import get_position_detail
return R.ok(data=get_position_detail(id))
# ============================================================
# 获取岗位列表
# ============================================================
@position_blue.route('/list', methods=['GET'])
@check_permission('sys:position:list')
def get_position_list():
"""
获取所有岗位列表(无分页,用于下拉选择)
:return: 岗位列表数据
"""
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')
def add():
"""
新增岗位
:return: 添加结果响应
"""
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)
# ============================================================
# 更新岗位
# ============================================================
@position_blue.route('/update', methods=['PUT'])
@check_permission('sys:position:update')
def update():
"""
更新岗位信息
:return: 更新结果响应
"""
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)
# ============================================================
# 删除岗位
# ============================================================
@position_blue.route('/delete/<int:id>', methods=['DELETE'])
@check_permission('sys:position:delete')
def delete(id):
"""
根据ID删除单个岗位
:param id: 岗位ID(路径参数)
:return: 删除结果响应
"""
from src.modules.system.position.service import delete_position
return delete_position(id)
# ============================================================
# 设置岗位状态
# ============================================================
@position_blue.route('/status', methods=['PUT'])
@check_permission('sys:position:status')
def status():
"""
修改岗位状态(启用/禁用)
:return: 状态更新结果响应
"""
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')
def batch_delete():
"""
批量删除岗位(请求体需传入 ID 数组)
Request Body 示例:
[1, 2, 3] # 岗位ID数组
:return: 批量删除结果响应
"""
from src.modules.system.position.service import delete_positions
id_list = parse_batch_ids(request)
return delete_positions(id_list)
```
:::tip 装饰器顺序
装饰器按从上到下执行:先 `@position_blue.route()`(路由),再 `@check_permission`(鉴权)。
:::
### 第二层:业务层(Service)
**目录位置**:`src/modules/{group}/{name}/service.py`
**核心职责**:
1. **业务逻辑编排**:组合多个数据操作完成业务流程
2. **唯一性校验**:通过 Schema 的 `validate_` 方法 + `Repository.exists_by()` 实现
3. **表单校验**:`_validate_form` 自动校验,`_clean_form_data` 过滤无效字段
4. **序列化加工**:`_detail_dict` / `_enrich_page_records` 加工响应数据
5. **文件处理**:文件字段迁移、URL 补全
6. **前置/后置钩子**:`before_add` / `before_update` / `before_delete` / `after_add` / `after_update` / `after_delete` 扩展点
**设计原则**:
- 通过 Repository 访问数据库,不直接使用 `db.query()`
- 返回 `R` 对象,不做 jsonify 封装
- 通过声明差异点定制通用 CRUD,减少重复代码
**代码示例**(岗位模块):
```python
# src/modules/system/position/service.py
# +======================================================================
# | 模块: 岗位业务逻辑层
# | 说明: 岗位的增删改查、唯一性校验、状态管理等业务处理
# +======================================================================
from typing import Optional
from src.common.base_service import BaseService
from src.modules.system.position.models import Position
from src.modules.system.position.repository import PositionRepository
from src.modules.system.user.models import User
from src.modules.system.user.repository import UserRepository
class PositionService(BaseService):
"""
岗位服务类
继承 BaseService 获得标准 CRUD 能力,并扩展删除前用户引用校验。
"""
repository = PositionRepository
form = PositionForm
status_form = PositionStatusForm
model_name = "岗位"
@classmethod
def before_delete(cls, ids):
"""删除前校验:存在用户引用该岗位时禁止删除"""
for pid in ids:
if UserRepository.count_by({'position_id': pid}) > 0:
raise BizError(f"岗位ID {pid} 已被用户引用,无法删除")
# ============================================================
# 模块级代理函数:保持 router 调用兼容
# ============================================================
get_position_page = PositionService.get_page
get_position_detail = PositionService.get_detail
get_info = PositionService.get_info
add_position = PositionService.add
update_position = PositionService.update
delete_positions = PositionService.remove
update_position_status = PositionService.update_status
get_position_list = PositionService.get_list
```
:::tip Service 层的两种模式
简单 CRUD 模块继承 `BaseService`,只需声明 `repository` / `form` / `model_name` 并覆写钩子方法。复杂模块(如用户)在继承基础上覆写 `add` / `update` 等方法。
:::
### 第三层:数据层(Repository)
**目录位置**:`src/modules/{group}/{name}/repository.py`
**核心职责**:
1. **CRUD 操作**:`create` / `update` / `batch_delete` / `get_by_id` 等
2. **查询构建**:`filter` / `filter_by` / `get_one` / `get_all` / `count`
3. **分页查询**:`paginate` 返回 `(items, total)` 元组
4. **软删除过滤**:等值方法自动追加 `is_delete=0`
5. **数据清洗**:`_clean_form_data` 过滤无效字段,`convert_camel_to_snake` 转命名
**设计原则**:
- 只做数据访问原语,不做 R 封装
- 等值方法自动软删过滤,`filter()` 为原始出口
- 删除的 R 响应由 Service 层统一封装
**代码示例**(岗位模块):
```python
# src/modules/system/position/repository.py
from src.common.base_repository import BaseRepository
from src.common.constants.common import PAGE_SIZE
from src.modules.system.position.models import Position
class PositionRepository(BaseRepository):
"""岗位数据仓储"""
model = Position
@classmethod
def page(cls, params: dict):
"""岗位分页查询"""
return cls.find_page(
page=int(params.get('pageNo', 1)),
page_size=int(params.get('pageSize', PAGE_SIZE)),
filters={'status': params.get('status')},
like_filters={'name': params.get('name')},
order_by='sort',
)
@classmethod
def list_enabled(cls, params=None):
"""查询启用的岗位列表"""
params = params or {}
return cls.find_list(
filters={'status': 1},
like_filters={'name': params.get('name')},
order_by='sort',
)
```
:::tip Repository 基类提供的方法
`BaseRepository` 提供以下通用方法,所有方法均为 `classmethod`:
- **查询**:`get_by_id` / `get_by_ids` / `first_by` / `exists_by` / `count_by` / `exists`
- **分页**:`find_page` 返回 `(items, total, pages)`
- **列表**:`find_list` 返回实例列表
- **写操作**:`create` / `update_by_id` / `update_status` / `delete_by_id` / `batch_delete` / `batch_restore`
:::
## 依赖方向
```
HTTP 层 ──依赖──→ Service 层 ──依赖──→ Repository 层 ──依赖──→ Model
│ │ │
│ │ │
▼ ▼ ▼
R (响应封装) R (响应封装) BaseRepository (基类)
WTForms BaseRepository db.session
check_permission BaseService SQLAlchemy ORM
```
:::warning 依赖规则
- HTTP 层只依赖 Service 层,不直接调用 Repository
- Service 层通过 Repository 访问数据库,不直接使用 `db.query()`
- Repository 层只依赖 Model 和数据库会话
- 下层不感知上层存在,不调用上层方法
:::
## 模型层(Model)
**目录位置**:`src/modules/{group}/{name}/models.py`
模型层定义数据库表结构,继承 `BaseModel`(通用字段)和 `BaseDB`(持久化方法)。
```python
# src/modules/system/position/models.py
from sqlalchemy import Column, String, Integer, text
from src.common.base_db import BaseDB
from src.common.base_model import BaseModel
from config.database import DB_PREFIX
# ============================================================
# 岗位模型
# ============================================================
class Position(BaseModel, BaseDB):
"""岗位模型类"""
# ============================================================
# 表名配置
# ============================================================
__tablename__ = DB_PREFIX + "position"
__table_comment__ = "岗位表"
# ============================================================
# 字段定义
# ============================================================
# 岗位名称
name = Column(String(255), nullable=False, index=True, comment="岗位名称")
# 岗位状态:1-在用 2-停用
status = Column(Integer, default=0, server_default=text('0'), index=True, comment="岗位状态:1-在用 2-停用")
# 岗位排序
sort = Column(Integer, default=0, server_default=text('0'), comment="岗位排序")
# ============================================================
# 内置方法
# ============================================================
def __repr__(self):
"""返回岗位名称作为字符串表示"""
return '岗位:{}'.format(self.name)
```
## Schema 层(校验)
**目录位置**:`src/modules/{group}/{name}/schema.py`
Schema 层定义请求参数的校验规则,使用 WTForms 的表单类 + validators。
```python
# src/modules/system/position/schema.py
# +======================================================================
# | 模块: 岗位表单验证
# | 说明: 岗位创建/编辑/状态更新的请求数据校验
# +======================================================================
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')
])
```
## 完整调用链路示例
以添加岗位为例,展示三层之间的调用关系:
```
POST /api/v1/position/add
│
├─ 1. Router (position.py)
│ @check_permission('sys:position:add')
│ def add():
│ data = parse_request_body(request, PositionForm)
│ return add_position(data)
│
├─ 2. Service (service.py)
│ @classmethod
│ def add(cls, data):
│ form = cls._validate_form(data) # 表单校验
│ extra = cls.before_add(form) or {} # 前置钩子
│ create_data = cls._clean_form_data(form)
│ create_data.update(extra)
│ obj = cls.repository.create(**create_data)
│ cls.after_add(obj) # 后置钩子
│
├─ 3. Repository (base_repository.py)
│ @classmethod
│ def create(cls, **kwargs):
│ obj = cls.model(**kwargs)
│ obj.save() # add + flush
│ return obj
│
├─ 4. Model (base_db.py)
│ def save(self):
│ db.session.add(self)
│ db.session.flush() # SQL 发送到数据库
│
└─ 5. Database
INSERT INTO flask_position (name, status, sort, create_user, ...)
VALUES ('高级工程师', 1, 10, '管理员', ...)
```
## 各层文件职责一览
| 文件 | 层次 | 职责 | 是否包含业务逻辑 |
|:---|:---:|:---|:---:|
| `position/router.py` | HTTP | 路由、权限、参数接收 | 否 |
| `position/schema.py` | HTTP | 请求参数校验规则 | 否 |
| `position/service.py` | 业务 | 业务逻辑、校验、序列化 | 是 |
| `position/repository.py` | 数据 | 数据库 CRUD、查询构建 | 否 |
| `position/models.py` | 数据 | 表结构定义、字段映射 | 否 |
## 总结
三层架构通过依赖单向和职责单一实现了良好的关注点分离。HTTP 层只关心请求接收和响应返回,Service 层只关心业务逻辑编排,Repository 层只关心数据访问。这种分层使得每一层都可以独立测试、独立替换,系统在模块数量增长时仍能保持清晰的代码结构。以岗位模块为例,完整实现一个 CRUD 模块只需 5 个文件,其中 Repository 和 Model 几乎零代码,开发者只需关注 Service 层的业务差异点。