Skip to content

扩展性设计 ​

本章详细描述系统的扩展性设计,包括模块可插拔、数据库驱动可切换、中间件可插拔三大扩展维度。

扩展性设计原则

遵循开闭原则(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 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 中添加:

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 包
mysqlmysql+pymysql://user:pass@host:port/db?charset=utf8mb4PyMySQL
postgresqlpostgresql+psycopg2://user:pass@host:port/dbpsycopg2-binary
mssqlmssql+pymssql://user:pass@host:port/db?charset=utf8pymssql
oracleoracle+oracledb://user:pass@host:port/?service_name=dboracledb
sqlitesqlite:///./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

驱动切换注意事项

  1. 切换驱动后需安装对应的 Python 包(如 psycopg2-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__)

    # 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

新增中间件示例 ​

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注入 ← 路由处理 ←┘

中间件注册顺序

Flask 中间件按注册顺序执行。CORS 最先配置 → 最外层(最先处理请求)。register_middleware 最后注册 → 最内层(最后处理请求)。

四、装饰器扩展 ​

自定义装饰器 ​

通过 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 行路由注册,不修改任何现有代码。数据库驱动通过环境变量切换,中间件通过注册机制插拔。这种设计使得系统在功能持续增长的同时,保持代码结构的清晰和稳定。

小蚂蚁云团队 · 提供技术支持