Become a sponsor

本章详细描述系统的扩展性设计,包括模块可插拔、数据库驱动可切换、中间件可插拔三大扩展维度。
扩展性设计原则
遵循开闭原则(OCP):对扩展开放,对修改关闭。新功能通过添加新模块实现,而非修改现有代码。核心框架通过基类、装饰器、中间件等机制提供扩展点,业务模块只需声明差异即可接入。
每个业务模块遵循固定的五文件结构,新增模块只需创建这五个文件即可接入系统:
src/modules/{group}/{name}/
├── models.py # ORM 模型
├── repository.py # 数据访问层
├── schema.py # 请求校验
├── service.py # 业务逻辑层
└── router.py # 路由定义以新增「培训管理(Training)」模块为例。
步骤一:创建模块目录和文件
创建 src/modules/system/training/ 目录,包含以下五个文件:
models.py — ORM 模型:
# 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 — 数据访问层:
# 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 — 请求校验:
# 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 — 业务逻辑层:
# 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_statusrouter.py — 路由定义:
# src/modules/system/training/router.py
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
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_batch_ids(request)
return batch_delete_trainings(id_list)步骤二:注册路由
在 src/routes/v1.py 中添加:
# 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 的基础上添加自定义方法:
# 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() 按驱动构建连接串:
# 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+psycopg2://user:pass@host:port/db | psycopg2-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 和对应的连接参数:
# 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驱动切换注意事项
psycopg2-binary for PostgreSQL)scripts/migrate_db.py 工具# 干跑:核对迁移计划
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() 中按顺序注册。新增中间件只需:
src/middleware/ 目录创建中间件文件create_app() 中注册# src/app.py - create_app() 中间件注册顺序
def create_app():
app = Flask(__name__)
# CORS + ProxyFix(最外层)
CORS(app, ...)
app.wsgi_app = ProxyFix(app.wsgi_app, ...)
# 先注册 → 内层(最后处理请求)
register_operation_log(app) # 操作日志记录
register_transaction(app) # AOP 事务管理
register_v1(app) # 路由注册
register_extends(app) # 扩展初始化
register_middleware(app) # 登录校验 + 白名单
register_error_handlers(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
CORS(app, ...) # CORS(最后注册→最外层)
return app# 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# src/app.py
from src.middleware.custom_middleware import register_custom
def create_app():
# ... 其他中间件 ...
register_custom(app)
# ...请求进入 → CORS → 上传限制 → 计时 → 操作日志 → DB会话 → Redis注入 → 路由处理
│
响应返回 ← CORS ← 上传限制 ← 计时 ← 操作日志 ← DB会话 ← Redis注入 ← 路由处理 ←┘中间件注册顺序
Flask 中间件按注册顺序执行。CORS 最先配置 → 最外层(最先处理请求)。register_middleware 最后注册 → 最内层(最后处理请求)。
通过 check_permission 和 check_demo 的模式,可以创建自定义装饰器:
# 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 文件管理,新增配置项只需:
.env 文件中添加变量config/ 对应模块中读取# .env
CUSTOM_FEATURE_ENABLED=true
CUSTOM_TIMEOUT=30# config/custom.py
import os
CUSTOM_FEATURE_ENABLED = os.getenv("CUSTOM_FEATURE_ENABLED", "false").lower() == "true"
CUSTOM_TIMEOUT = int(os.getenv("CUSTOM_TIMEOUT", "30"))ui/src/api/ 创建 API 文件ui/src/views/ 创建页面组件flask_menu 表中配置菜单路由公共组件放在 ui/src/components/ 目录,遵循 Vue3 组件规范。
| 扩展维度 | 扩展方式 | 修改范围 | 复杂度 |
|---|---|---|---|
| 新增 CRUD 模块 | 创建 5 文件 + 注册路由 | 新增文件,不改现有代码 | 低 |
| 新增复杂模块 | 继承基类 + 自定义方法 | 新增文件,不改现有代码 | 中 |
| 切换数据库驱动 | 修改 .env 配置 | 仅改配置 | 低 |
| 新增中间件 | 创建中间件 + 注册 | 新增文件 + 改 app.py | 低 |
| 新增装饰器 | 创建装饰器 + 应用 | 新增文件 | 低 |
| 新增配置项 | .env + config 模块 | 新增配置 | 低 |
| 新增前端页面 | API + Views + Menu | 新增文件 + 改数据库 | 中 |
通过分层架构、基类模板方法、装饰器、中间件、环境变量配置等机制,实现了高度的可扩展性。新增 CRUD 模块只需 5 个标准文件 + 1 行路由注册,不修改任何现有代码。数据库驱动通过环境变量切换,中间件通过注册机制插拔。这种设计使得系统在功能持续增长的同时,保持代码结构的清晰和稳定。