# 扩展性设计 本章详细描述系统的扩展性设计,包括模块可插拔、数据库驱动可切换、中间件可插拔三大扩展维度。 :::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/', 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/', 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 行路由注册,不修改任何现有代码。数据库驱动通过环境变量切换,中间件通过注册机制插拔。这种设计使得系统在功能持续增长的同时,保持代码结构的清晰和稳定。