Files
djangoadmin-wiki/DjangoAdmin_FastAPI_EleVue/zh/volume3/layer-arch.md
T
2026-09-12 23:12:15 +08:00

508 lines
19 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 层 (Endpoint) │
│ api/v1/endpoints/position.py │
│ 职责:路由定义 · 权限装饰器 · 参数接收 · 响应返回 │
│ 原则:不含业务逻辑,仅做请求转发 │
└──────────────────────────┬──────────────────────────────────┘
│ 调用
▼
┌─────────────────────────────────────────────────────────────┐
│ 业务层 (Service) │
│ modules/system/position/service.py │
│ 职责:业务逻辑 · 校验规则 · 字段组装 · 序列化加工 │
│ 原则:不直接操作数据库,通过 Repository 访问数据 │
└──────────────────────────┬──────────────────────────────────┘
│ 调用
▼
┌─────────────────────────────────────────────────────────────┐
│ 数据层 (Repository) │
│ modules/system/position/repository.py │
│ 职责:数据库 CRUD · 查询条件组装 · 软删除过滤 │
│ 原则:只做数据访问原语,不做业务校验和响应封装 │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 数据库 (MySQL / PostgreSQL / etc.) │
└─────────────────────────────────────────────────────────────┘
```
## 各层详细职责
### 第一层:HTTP 层(Endpoint)
**目录位置**:`src/api/v1/endpoints/{module}.py`
**核心职责**:
1. **路由定义**:声明 HTTP 方法、URL 路径、请求参数类型
2. **权限控制**:通过 `@permission_required` 装饰器声明所需权限
3. **演示模式保护**:通过 `@check_demo` 装饰器阻止演示环境的写操作
4. **参数接收**:通过 Pydantic 模型自动校验请求体
5. **响应返回**:直接返回 Service 层的 `R` 对象
**设计原则**:
- 不包含任何业务逻辑
- 不直接访问数据库
- 仅做请求转发和响应返回
- 权限字符串遵循 `sys:{module}:{action}` 格式
**代码示例**(岗位模块):
```python
# src/api/v1/endpoints/position.py
# +======================================================================
# | 模块: 岗位路由层
# | 说明: 提供岗位信息的增删改查、状态管理、列表获取等 API 接口
# +======================================================================
from fastapi import APIRouter, Request
from modules.system.position.schemas import PositionForm, PositionStatusForm
from core.access_decorators import check_demo
from core.access_decorators import permission_required
from modules.system.position.service import position_service
from core import R
# ============================================================
# 创建路由实例
# ============================================================
router = APIRouter()
# ============================================================
# 查询分页数据
# ============================================================
@router.get('/page', summary='查询分页数据')
@permission_required("sys:position:page")
def page(request: Request):
"""
获取岗位分页列表
:param request: FastAPI请求对象
:return: 分页数据响应
"""
return position_service.get_page(request)
# ============================================================
# 查询岗位详情
# ============================================================
@router.get('/detail/{id}', summary='查询岗位详情')
@permission_required("sys:position:detail")
def detail(request: Request, id):
"""
根据岗位ID查询详细信息
:param request: FastAPI请求对象
:param id: 岗位ID(路径参数)
:return: 岗位详情数据
"""
return R.ok(data=position_service.get_detail(id))
# ============================================================
# 获取岗位列表
# ============================================================
@router.get('/list', summary='获取岗位列表')
@permission_required("sys:position:list")
def get_position_list(request: Request):
"""
获取所有岗位列表(无分页,用于下拉选择)
:param request: FastAPI请求对象
:return: 岗位列表数据
"""
return R.ok(data=position_service.get_position_list(request))
# ============================================================
# 添加岗位
# ============================================================
@router.post('/add', summary='添加岗位')
@permission_required("sys:position:add")
@check_demo
def add(request: Request, data: PositionForm):
"""
新增岗位
:param request: FastAPI请求对象
:param data: 岗位表单数据
:return: 添加结果响应
"""
return position_service.add(request, data)
# ============================================================
# 更新岗位
# ============================================================
@router.put('/update', summary='更新岗位')
@permission_required("sys:position:update")
@check_demo
def update(request: Request, data: PositionForm):
"""
更新岗位信息
:param request: FastAPI请求对象
:param data: 岗位表单数据(必须包含 id 字段)
:return: 更新结果响应
"""
return position_service.update(request, data)
# ============================================================
# 删除岗位
# ============================================================
@router.delete('/delete/{id}', summary='删除岗位')
@permission_required("sys:position:delete")
@check_demo
def delete(request: Request, id):
"""
根据ID删除单个岗位
:param request: FastAPI请求对象
:param id: 岗位ID(路径参数)
:return: 删除结果响应
"""
return position_service.delete(id)
# ============================================================
# 设置岗位状态
# ============================================================
@router.put('/status', summary='设置岗位状态')
@permission_required("sys:position:status")
@check_demo
def status(request: Request, data: PositionStatusForm):
"""
修改岗位状态(启用/禁用)
:param request: FastAPI请求对象
:param data: 状态表单数据
:return: 状态更新结果响应
"""
return position_service.update_status(request, data)
# ============================================================
# 批量删除岗位
# ============================================================
@router.delete('/batchDelete', summary='批量删除岗位')
@permission_required("sys:position:batchDelete")
@check_demo
async def batch_delete(request: Request):
"""
批量删除岗位(请求体需传入 ID 数组)
Request Body 示例:
[1, 2, 3] # 岗位ID数组
:param request: FastAPI请求对象
:return: 批量删除结果响应
"""
return await position_service.batch_delete(request)
```
:::tip 装饰器顺序
装饰器按从上到下执行:先 `@permission_required`(鉴权),再 `@check_demo`(演示模式),最后才是路由函数。批量删除端点为 `async def`,因为需要 `await parse_batch_ids(request)` 异步读取请求体。
:::
### 第二层:业务层(Service)
**目录位置**:`src/modules/{group}/{name}/service.py`
**核心职责**:
1. **业务逻辑编排**:组合多个数据操作完成业务流程
2. **唯一性校验**:通过 `unique_fields` 声明唯一约束
3. **字段组装**:`_build_create_fields` / `_build_update_fields` 组装入库字段
4. **序列化加工**:`_serialize` / `_serialize_detail` 加工响应数据
5. **文件处理**:文件字段迁移、URL 补全
6. **前置/后置钩子**:`_before_add` / `_before_update` / `_before_delete` 扩展点
**设计原则**:
- 通过 Repository 访问数据库,不直接使用 `db.query()`
- 返回 `R` 对象,不做 JSONResponse 封装
- 通过声明差异点定制通用 CRUD,减少重复代码
**代码示例**(岗位模块):
```python
# src/modules/system/position/service.py
# +======================================================================
# | 模块: 岗位业务逻辑层
# | 说明: 岗位的增删改查、唯一性校验、状态管理等业务处理
# +======================================================================
from typing import Optional
from core.base_service import BaseService
from modules.system.position.models import Position
from modules.system.position.repository import position_repo
from modules.system.user.models import User
from modules.system.user.repository import user_repo
from utils.request import parse_id_list
# ============================================================
# 岗位业务服务类
# ============================================================
class PositionService(BaseService[Position]):
"""岗位业务服务类,继承基础服务获得通用 CRUD 能力"""
# 数据访问与模型
repo = position_repo
model = Position
# 分页差异点
page_like_fields = ('name',)
page_eq_fields = ('status',)
page_order_by = (('sort', 'asc'),)
# 唯一性校验
unique_fields = {'name': '岗位名称不能重复'}
# ============================================================
# 删除前校验
# ============================================================
def _before_delete(self, ids) -> Optional[str]:
"""删除前校验:存在用户引用该岗位时禁止删除,避免用户岗位悬空"""
id_list = parse_id_list(str(ids))
if id_list and user_repo.filter(User.position_id.in_(id_list), User.is_delete == 0).first():
return "存在用户引用该岗位,请先调整用户岗位"
return None
# ============================================================
# 获取岗位数据列表
# ============================================================
def get_position_list(self, request):
"""获取岗位数据列表"""
# 与 /page 一致的筛选条件:name 模糊、status 精确;保留软删过滤与排序
query = self._apply_page_filters(self.repo.filter_by(), request)
query = query.order_by(self.model.sort.asc())
# 返回结果
return [v.to_dict() for v in query.all()]
# ============================================================
# 模块级单例
# ============================================================
position_service = PositionService()
```
:::tip Service 层的两种模式
简单 CRUD 模块继承 `BaseService`,只需声明差异点即可获得完整的增删改查能力。复杂模块(如用户、角色、菜单)在继承基础上添加自定义方法,或完全自定义 Service 函数。
:::
### 第三层:数据层(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_model_data` 将 camelCase 转 snake_case
**设计原则**:
- 只做数据访问原语,不做 R 封装
- 等值方法自动软删过滤,`filter()` 为原始出口
- 删除的 R 响应由 Service 层 `batch_delete_with_r` 统一产出
**代码示例**(岗位模块):
```python
# src/modules/system/position/repository.py
# +======================================================================
# | 模块: 岗位数据访问层
# | 说明: 封装岗位模型的数据访问操作
# +======================================================================
"""modules/system/position 数据访问层。"""
from modules.system.position.models import Position
from core.base_repository import BaseRepository
# ============================================================
# 岗位仓库类
# ============================================================
class PositionRepository(BaseRepository[Position]):
"""岗位数据仓库,继承基础仓库获得通用 CRUD 能力"""
pass
# ============================================================
# 仓库实例
# ============================================================
position_repo = PositionRepository(Position)
```
:::tip Repository 基类提供的方法
`BaseRepository` 提供以下通用方法,子类可直接使用:
- **查询**:`get_by_id` / `get_by_ids` / `get_one` / `get_all` / `count` / `exists` / `exists_by_field`
- **分页**:`paginate` 返回 `(items, total)`
- **写操作**:`create` / `update` / `batch_delete`
- **条件构建**:`filter`(原始出口) / `filter_by`(等值出口)
:::
## 依赖方向
```
HTTP 层 ──依赖──→ Service 层 ──依赖──→ Repository 层 ──依赖──→ Model
│ │ │
│ │ │
▼ ▼ ▼
R (响应封装) R (响应封装) BaseRepository (基类)
Pydantic BaseRepository get_db_session()
AccessDecorator BaseService SQLAlchemy ORM
```
:::warning 依赖规则
- HTTP 层只依赖 Service 层,不直接调用 Repository
- Service 层通过 Repository 访问数据库,不直接使用 `db.query()`
- Repository 层只依赖 Model 和数据库会话
- 下层不感知上层存在,不调用上层方法
:::
## 模型层(Model)
**目录位置**:`src/modules/{group}/{name}/models.py`
模型层定义数据库表结构,继承 `base_model`(通用字段)和 `base_db`(持久化方法)。
```python
# src/modules/system/position/models.py
from sqlalchemy import Column, String, Integer, text
from core.base_db import base_db
from core.base_model import base_model
from core.config import DB_PREFIX
# ============================================================
# 岗位模型
# ============================================================
class Position(base_model, base_db):
"""岗位模型类"""
# ============================================================
# 表名配置
# ============================================================
__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}/schemas.py`
Schema 层定义请求参数的校验规则,使用 Pydantic v2 的 `BaseModel` + `Field`。
```python
# src/modules/system/position/schemas.py
# +======================================================================
# | 模块: 岗位表单验证
# | 说明: 岗位创建/编辑/状态更新的请求数据校验
# +======================================================================
from typing import Optional
from pydantic import BaseModel, Field
from core.base_schemas import BaseForm
# ============================================================
# 岗位表单类
# ============================================================
class PositionForm(BaseForm):
"""岗位创建/编辑表单"""
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-停用")
```
## 完整调用链路示例
以添加岗位为例,展示三层之间的调用关系:
```
POST /api/v1/position/add
│
├─ 1. Endpoint (position.py)
│ @permission_required("sys:position:add")
│ @check_demo
│ def add(request, data: PositionForm):
│ return position_service.add(request, data)
│
├─ 2. Service (service.py)
│ def add(self, request, data):
│ err = self._check_unique(data) # 唯一性校验
│ self._before_add(request, data) # 前置钩子
│ fields = self._build_create_fields() # 字段组装
│ self.repo.create(**fields) # 调用 Repository
│ return R.ok(msg='添加成功')
│
├─ 3. Repository (base_repository.py)
│ def create(self, **fields):
│ obj = self.model(**self._clean_model_data(fields))
│ obj.save() # add + flush
│ return obj
│
├─ 4. Model (base_db.py)
│ def save(self):
│ db = get_db_session()
│ db.add(self)
│ db.flush() # SQL 发送到数据库
│
└─ 5. Database
INSERT INTO fastapi_position (name, status, sort, create_user, ...)
VALUES ('高级工程师', 1, 10, '管理员', ...)
```
## 各层文件职责一览
| 文件 | 层次 | 职责 | 是否包含业务逻辑 |
|:---|:---:|:---|:---:|
| `endpoints/position.py` | HTTP | 路由、权限、参数接收 | 否 |
| `position/schemas.py` | HTTP | 请求参数校验规则 | 否 |
| `position/service.py` | 业务 | 业务逻辑、校验、序列化 | 是 |
| `position/repository.py` | 数据 | 数据库 CRUD、查询构建 | 否 |
| `position/models.py` | 数据 | 表结构定义、字段映射 | 否 |
## 总结
三层架构通过依赖单向和职责单一实现了良好的关注点分离。HTTP 层只关心请求接收和响应返回,Service 层只关心业务逻辑编排,Repository 层只关心数据访问。这种分层使得每一层都可以独立测试、独立替换,系统在模块数量增长时仍能保持清晰的代码结构。以岗位模块为例,完整实现一个 CRUD 模块只需 5 个文件,其中 Repository 和 Model 几乎零代码,开发者只需关注 Service 层的业务差异点。