Files
djangoadmin-wiki/DjangoAdmin_Flask_NaiveVue/zh/volume3/extensibility.md
T
2026-09-12 23:12:15 +08:00

585 lines
18 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.
# 扩展性设计
本章详细描述系统的扩展性设计,包括模块可插拔、数据库驱动可切换、中间件可插拔三大扩展维度。
:::tip 扩展性设计原则
遵循开闭原则(OCP):对扩展开放,对修改关闭。新功能通过添加新模块实现,而非修改现有代码。核心框架通过基类、装饰器、中间件等机制提供扩展点,业务模块只需声明差异即可接入。
:::
## 一、模块可插拔设计
### 模块标准结构
每个业务模块遵循固定的五文件结构,新增模块只需创建这五个文件即可接入系统:
```
src/modules/{group}/{name}/
├── models.py # ORM 模型
├── repository.py # 数据访问层
├── schema.py # 请求校验
├── service.py # 业务逻辑层
└── router.py # 路由定义
```
### 新增模块步骤
以新增「培训管理(Training)」模块为例。
**步骤一:创建模块目录和文件**
创建 `src/modules/system/training/` 目录,包含以下五个文件:
`models.py` — ORM 模型:
```python
# src/modules/system/training/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 Training(BaseModel, BaseDB):
__tablename__ = DB_PREFIX + "training"
__table_comment__ = "培训记录表"
title = Column(String(200), nullable=False, comment="培训标题")
content = Column(String(2000), nullable=True, comment="培训内容")
trainer = Column(String(50), nullable=True, comment="培训讲师")
status = Column(Integer, default=1, server_default=text('1'), comment="状态")
sort = Column(Integer, default=0, server_default=text('0'), comment="排序")
```
`repository.py` — 数据访问层:
```python
# src/modules/system/training/repository.py
from src.modules.system.training.models import Training
from src.common.base_repository import BaseRepository
class TrainingRepository(BaseRepository):
model = Training
_training_repository = TrainingRepository()
```
`schema.py` — 请求校验:
```python
# src/modules/system/training/schema.py
from wtforms import StringField, IntegerField
from wtforms.validators import DataRequired, Length, NumberRange
from src.common.forms.base_form import BaseForm
class TrainingForm(BaseForm):
title = StringField('培训标题', validators=[
DataRequired(message='培训标题不能为空'),
Length(min=1, max=200, message='培训标题长度必须在1-200个字符之间')
])
content = StringField('培训内容', validators=[
Length(max=2000, message='培训内容长度不能超过2000个字符')
])
trainer = StringField('培训讲师', validators=[
Length(max=50, message='培训讲师长度不能超过50个字符')
])
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 TrainingStatusForm(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')
])
```
`service.py` — 业务逻辑层:
```python
# src/modules/system/training/service.py
from src.common.base_service import BaseService
from src.modules.system.training.models import Training
from src.modules.system.training.repository import TrainingRepository
class TrainingService(BaseService):
repository = TrainingRepository
form = TrainingForm
status_form = TrainingStatusForm
model_name = "培训"
# 模块级代理函数
get_training_page = TrainingService.get_page
get_training_detail = TrainingService.get_detail
add_training = TrainingService.add
update_training = TrainingService.update
delete_trainings = TrainingService.remove
update_training_status = TrainingService.update_status
```
`router.py` — 路由定义:
```python
# src/modules/system/training/router.py
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
training_blue = Blueprint('training', __name__, url_prefix='/training')
@training_blue.route('/page', methods=['GET'])
@check_permission('sys:training:page')
def page():
from src.modules.system.training.service import get_training_page
return get_training_page()
@training_blue.route('/detail/<int:id>', methods=['GET'])
@check_permission('sys:training:detail')
def detail(id):
from src.modules.system.training.service import get_training_detail
return R.ok(data=get_training_detail(id))
@training_blue.route('/add', methods=['POST'])
@check_permission('sys:training:add')
def add():
from src.modules.system.training.service import add_training
from src.modules.system.training.schema import TrainingForm
data = parse_request_body(request, TrainingForm)
return add_training(data)
@training_blue.route('/update', methods=['PUT'])
@check_permission('sys:training:update')
def update():
from src.modules.system.training.service import update_training
from src.modules.system.training.schema import TrainingForm
data = parse_request_body(request, TrainingForm)
return update_training(data)
@training_blue.route('/delete/<int:id>', methods=['DELETE'])
@check_permission('sys:training:delete')
def delete(id):
from src.modules.system.training.service import delete_training
return delete_training(id)
@training_blue.route('/status', methods=['PUT'])
@check_permission('sys:training:status')
def status():
from src.modules.system.training.service import update_training_status
from src.modules.system.training.schema import TrainingStatusForm
data = parse_request_body(request, TrainingStatusForm)
return update_training_status(data)
@training_blue.route('/batchDelete', methods=['DELETE'])
@check_permission('sys:training:batchDelete')
def batch_delete():
from src.modules.system.training.service import batch_delete_trainings
id_list = parse_id_list(request)
return batch_delete_trainings(id_list)
```
**步骤二:注册路由**
在 `src/routes/v1.py` 中添加:
```python
# src/routes/v1.py
from src.modules.system.training.router import training_blue
BLUEPRINTS = [
# ... 其他模块
training_blue,
]
```
路由注册后,该模块的所有端点自动挂在 `/api/v1/training` 前缀下,并通过 `src/routes/v1.py` 的 `register_v1()` 统一挂载到 Flask 应用实例。
**步骤三:配置菜单权限**
在数据库 `flask_menu` 表中插入对应的菜单和权限节点记录。权限标识格式为 `sys:{module}:{action}`,如 `sys:training:add`、`sys:training:page`。
### 模块解耦
模块之间通过以下方式解耦:
| 解耦方式 | 说明 | 示例 |
|:---|:---|:---|
| **Repository 单例** | 模块级单例,通过 import 引用 | `from src.modules.system.user.repository import UserRepository` |
| **Service 代理函数** | 模块级代理函数,通过 import 引用 | `from src.modules.system.user.service import get_user_page` |
| **跨模块引用** | 通过 Repository 引用其他模块的模型 | PositionService 引用 User 模型检查引用 |
| **事件机制** | 暂未实现(未来可通过事件总线解耦) | — |
### 复杂模块扩展
对于业务逻辑复杂的模块(如 User、Role、Menu),在继承 `BaseService` 的基础上添加自定义方法:
```python
# src/modules/system/user/service.py
class UserService(BaseService):
repository = UserRepository
form = UserForm
status_form = UserStatusForm
model_name = "用户"
@classmethod
def add(cls, data: dict) -> None:
"""新增用户(覆写基类方法)"""
form = cls._validate_form(data)
# 密码加密
pwd = form.data.get("password") or DEFAULT_PASSWORD
salt = generate_salt()
create_data = cls._clean_form_data(form)
create_data["password"] = encrypt_password(pwd, salt)
create_data["salt"] = salt
create_data["create_user"] = get_realname()
user = cls.repository.create(**create_data)
user.code = f"{user.id:06d}"
@classmethod
def _detail_dict(cls, obj):
"""详情序列化:剔除密码字段,补充关联数据"""
data = model_to_dict(obj)
if data:
data.pop("password", None)
return _enrich_user_detail(data)
@classmethod
def before_delete(cls, ids):
"""删除前校验:超管禁止删除"""
if 1 in ids:
raise BizError("超级管理员不能删除")
```
## 二、数据库驱动可切换
### 驱动注册机制
项目通过 `_SUPPORTED_DRIVERS` 声明支持的驱动,`_normalize_driver()` 处理别名映射和校验,`_build_database_url()` 按驱动构建连接串:
```python
# config/database.py(简化)
import os
from urllib.parse import quote_plus
# 支持的数据库驱动
_SUPPORTED_DRIVERS = ("mysql", "postgresql", "mssql", "sqlite", "oracle")
def _normalize_driver(driver: str) -> str:
"""归一化 DB_DRIVER:别名映射 + 校验"""
name = str(driver).strip().lower()
alias_map = {
'postgres': 'postgresql',
'pg': 'postgresql',
'sqlserver': 'mssql',
'sql_server': 'mssql',
'sqlite3': 'sqlite',
}
name = alias_map.get(name, name)
if name not in _SUPPORTED_DRIVERS:
raise ValueError(f"不支持的 DB_DRIVER: {driver!r}")
return name
DB_DRIVER = _normalize_driver(os.getenv('DB_DRIVER', 'mysql'))
DB_HOST = os.getenv('DB_HOST', '127.0.0.1')
DB_PORT = int(os.getenv('DB_PORT', '3306'))
DB_DATABASE = os.getenv('DB_DATABASE', 'djangoadmin.flask.elevue')
DB_USERNAME = os.getenv('DB_USERNAME', 'root')
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
def _build_database_url(driver: str = None) -> str:
"""按驱动构建 SQLAlchemy 连接串"""
drv = _normalize_driver(driver if driver is not None else DB_DRIVER)
if drv == 'sqlite':
return 'sqlite:///./' + DB_DATABASE
base = quote_plus(DB_USERNAME) + ':' + quote_plus(DB_PASSWORD) + '@' + DB_HOST + ':' + str(DB_PORT)
if drv == 'mysql':
return 'mysql+pymysql://' + base + '/' + DB_DATABASE + '?charset=utf8mb4'
if drv == 'postgresql':
return 'postgresql+psycopg://' + base + '/' + DB_DATABASE
if drv == 'oracle':
return 'oracle+oracledb://' + base + '/?service_name=' + quote_plus(DB_DATABASE)
# mssql
return 'mssql+pymssql://' + base + '/' + DB_DATABASE + '?charset=utf8'
```
各驱动的连接串格式:
| 驱动 | 连接串格式 | Python 包 |
|:----|:----|:----|
| mysql | `mysql+pymysql://user:pass@host:port/db?charset=utf8mb4` | PyMySQL |
| postgresql | `postgresql+psycopg://user:pass@host:port/db` | psycopg[binary] |
| mssql | `mssql+pymssql://user:pass@host:port/db?charset=utf8` | pymssql |
| oracle | `oracle+oracledb://user:pass@host:port/?service_name=db` | oracledb |
| sqlite | `sqlite:///./path/to/db` | 内置 |
### 切换方式
只需修改 `.env` 文件中的 `DB_DRIVER` 和对应的连接参数:
```bash
# MySQL(默认)
DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=djangoadmin
DB_USERNAME=root
DB_PASSWORD=your_password
# PostgreSQL
DB_DRIVER=postgresql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=djangoadmin
DB_USERNAME=postgres
DB_PASSWORD=your_password
# SQLite
DB_DRIVER=sqlite
DB_DATABASE=./data.db
# SQL Server
DB_DRIVER=mssql
DB_HOST=127.0.0.1
DB_PORT=1433
DB_DATABASE=djangoadmin
DB_USERNAME=sa
DB_PASSWORD=your_password
```
:::warning 驱动切换注意事项
1. 切换驱动后需安装对应的 Python 包(如 `psycopg[binary]` for PostgreSQL)
2. 数据迁移使用 `scripts/migrate_db.py` 工具
3. SQLite 不支持并发写入,仅适用于开发/测试环境
4. Oracle 需要额外的客户端库配置
:::
### 跨库数据迁移
```bash
# 干跑:核对迁移计划
python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver postgresql --dst-host ... --dry-run
# 执行迁移
python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver postgresql --dst-host ...
# 幂等重跑(先删除目标库数据)
python scripts/migrate_db.py --src-driver mysql --src-host ... --dst-driver postgresql --dst-host ... --drop-target-first
```
## 三、中间件可插拔
### 中间件注册机制
所有中间件在 `src/app.py` 的 `create_app()` 中按顺序注册。新增中间件只需:
1. 在 `src/middleware/` 目录创建中间件文件
2. 在 `create_app()` 中注册
```python
# src/app.py - create_app() 中间件注册顺序
def create_app():
app = Flask(__name__)
# 先注册 → 内层(最后处理请求)
register_redis(app) # Redis 上下文注入
register_db_session(app) # DB 会话管理(commit/rollback)
register_operation_log(app) # 操作日志记录
register_transaction(app) # AOP 事务管理
register_v1(app) # 路由注册(login_required 依赖)
register_exception(app) # 全局异常处理器
# 请求计时(先注册→内层,统计业务耗时)
@app.before_request
def start_timer():
g.start_time = time.time()
@app.after_request
def add_process_time_header(response):
if hasattr(g, 'start_time'):
process_time = time.time() - g.start_time
response.headers["X-Process-Time"] = str(round(process_time, 5))
return response
register_rate_limit(app) # 滑动窗口限流
register_cors(app) # CORS(最后注册→最外层)
return app
```
### 新增中间件示例
```python
# src/middleware/custom_middleware.py
from flask import request, g
def register_custom(app):
"""注册自定义中间件"""
@app.before_request
def custom_before():
"""请求前逻辑"""
g.custom_data = "xxx"
@app.after_request
def custom_after(response):
"""响应后逻辑"""
response.headers["X-Custom-Header"] = "value"
return response
```
```python
# src/app.py
from src.middleware.custom_middleware import register_custom
def create_app():
# ... 其他中间件 ...
register_custom(app)
# ...
```
### 中间件执行顺序
```
请求进入 → CORS → 限流 → 上传限制 → 计时 → 操作日志 → DB会话 → Redis注入 → 路由处理
│
响应返回 ← CORS ← 限流 ← 上传限制 ← 计时 ← 操作日志 ← DB会话 ← Redis注入 ← 路由处理 ←┘
```
:::warning 中间件注册顺序
Flask 中间件按注册顺序执行。`register_cors` 最后注册 → 最外层(最先处理请求)。`register_redis` 最先注册 → 最内层(最后处理请求)。
:::
## 四、装饰器扩展
### 自定义装饰器
通过 `check_permission` 和 `check_demo` 的模式,可以创建自定义装饰器:
```python
# src/common/decorators.py
import time
from functools import wraps
def log_operation(module: str, action: str):
"""
操作日志记录装饰器
自动记录接口调用的模块、操作类型、执行耗时
Args:
module: 所属模块名称,如 "培训管理"
action: 操作类型,如 "新增"、"编辑"、"删除"
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 记录开始时间,用于计算接口执行耗时
start = time.time()
# 执行原业务函数
result = func(*args, **kwargs)
# 计算耗时(秒)
duration = time.time() - start
# 记录操作日志到数据库(包含模块、操作、耗时等信息)
save_log(module, action, duration)
return result
return wrapper
return decorator
# src/modules/system/training/router.py
# 使用示例:多个装饰器叠加使用
# 执行顺序:从下往上执行(先 log_operation,再 check_permission)
@training_blue.route('/add', methods=['POST'])
@check_permission('sys:training:add') # 权限校验:需要培训新增权限
@log_operation("培训管理", "新增") # 操作日志记录:记录本次操作
def add():
"""新增培训记录接口"""
from src.modules.system.training.service import add_training
from src.modules.system.training.schema import TrainingForm
data = parse_request_body(request, TrainingForm)
return add_training(data)
```
## 五、配置扩展
### 环境变量驱动
所有可配置项通过 `.env` 文件管理,新增配置项只需:
1. 在 `.env` 文件中添加变量
2. 在 `config/` 对应模块中读取
3. 在业务代码中引用
```bash
# .env
CUSTOM_FEATURE_ENABLED=true
CUSTOM_TIMEOUT=30
```
```python
# config/custom.py
import os
CUSTOM_FEATURE_ENABLED = os.getenv("CUSTOM_FEATURE_ENABLED", "false").lower() == "true"
CUSTOM_TIMEOUT = int(os.getenv("CUSTOM_TIMEOUT", "30"))
```
## 六、前端扩展
### 新增页面
1. 在 `ui/src/api/` 创建 API 文件
2. 在 `ui/src/views/` 创建页面组件
3. 在数据库 `flask_menu` 表中配置菜单路由
### 新增组件
公共组件放在 `ui/src/components/` 目录,遵循 Vue3 组件规范。
## 扩展性矩阵
| 扩展维度 | 扩展方式 | 修改范围 | 复杂度 |
|:---|:---|:---|:---|
| 新增 CRUD 模块 | 创建 5 文件 + 注册路由 | 新增文件,不改现有代码 | 低 |
| 新增复杂模块 | 继承基类 + 自定义方法 | 新增文件,不改现有代码 | 中 |
| 切换数据库驱动 | 修改 .env 配置 | 仅改配置 | 低 |
| 新增中间件 | 创建中间件 + 注册 | 新增文件 + 改 app.py | 低 |
| 新增装饰器 | 创建装饰器 + 应用 | 新增文件 | 低 |
| 新增配置项 | .env + config 模块 | 新增配置 | 低 |
| 新增前端页面 | API + Views + Menu | 新增文件 + 改数据库 | 中 |
## 总结
通过分层架构、基类模板方法、装饰器、中间件、环境变量配置等机制,实现了高度的可扩展性。新增 CRUD 模块只需 5 个标准文件 + 1 行路由注册,不修改任何现有代码。数据库驱动通过环境变量切换,中间件通过注册机制插拔。这种设计使得系统在功能持续增长的同时,保持代码结构的清晰和稳定。