# 扩展性设计 本章详细描述系统的扩展性设计,包括模块可插拔、数据库驱动可切换、中间件可插拔三大扩展维度。 :::tip 扩展性设计原则 遵循开闭原则(OCP):对扩展开放,对修改关闭。新功能通过添加新模块实现,而非修改现有代码。核心框架通过基类、装饰器、中间件等机制提供扩展点,业务模块只需声明差异即可接入。 ::: ## 一、模块可插拔设计 ### 模块标准结构 每个业务模块遵循固定的四文件结构,新增模块只需创建这四个文件即可接入系统: ``` src/modules/{group}/{name}/ ├── models.py # ORM 模型 ├── repository.py # 数据访问层 ├── schemas.py # 请求校验 └── service.py # 业务逻辑层 ``` ### 新增模块步骤 以新增「培训管理(Training)」模块为例。 **步骤一:创建模块目录和文件** 创建 `src/modules/training/` 目录,包含以下四个文件: `models.py` — ORM 模型: ```python # src/modules/training/models.py from sqlalchemy import Column, String, Integer, text from core.base_model import BaseModel from core.base_model import base_model from core.config import DB_PREFIX class Training(BaseModel): __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/training/repository.py from training.models import Training from core.base_repository import BaseRepository class TrainingRepository(BaseRepository(Training]): pass training_repo = TrainingRepository(Training) ``` `schemas.py` — 请求校验: ```python # src/modules/training/schemas.py from pydantic import BaseModel, Field from core.base_schema import BaseSchema class TrainingForm(BaseSchema): title: str = Field(..., min_length=1, max_length=200, description="培训标题") content: str = Field(None, max_length=2000, description="培训内容") trainer: str = Field(None, max_length=50, description="培训讲师") status: int = Field(..., ge=1, le=2, description="状态") sort: int = Field(..., ge=0, le=99999, description="排序") class TrainingStatusForm(BaseModel): id: int = Field(..., gt=0, description="培训ID") status: int = Field(..., ge=1, le=2, description="状态") ``` `service.py` — 业务逻辑层: ```python # src/modules/training/service.py from core.base_service import BaseService from training.models import Training from training.repository import training_repo class TrainingService(BaseService(Training]): repo = training_repo model = Training page_like_fields = ('title', 'trainer') page_eq_fields = ('status',) page_order_by = (('sort', 'asc'),) unique_fields = {'title': '培训标题不能重复'} training_service = TrainingService() ``` **步骤二:创建 Handler** 创建 `src/modules/training/handlers.py`: ```python # src/modules/training/handlers.py from common.middleware.access_decorators import check_demo, permission_required from common.middleware.operation_log import operation_log from core import response as R from core.base_handler import BaseHandler from modules.training.service import training_service class TrainingPageHandler(BaseHandler): @permission_required("sys:training:page") async def get(self): return await training_service.get_page(self) class TrainingDetailHandler(BaseHandler): @permission_required("sys:training:detail") async def get(self, id): return R.ok(self, data=training_service.get_detail(id)) class TrainingAddHandler(BaseHandler): @permission_required("sys:training:add") @check_demo @operation_log("培训管理", "添加") async def post(self): return await training_service.add(self) class TrainingUpdateHandler(BaseHandler): @permission_required("sys:training:update") @check_demo @operation_log("培训管理", "更新") async def put(self): return await training_service.update(self) class TrainingDeleteHandler(BaseHandler): @permission_required("sys:training:delete") @check_demo @operation_log("培训管理", "删除") async def delete(self, id): return await training_service.delete(self, id) class TrainingStatusHandler(BaseHandler): @permission_required("sys:training:status") @check_demo @operation_log("培训管理", "设置状态") async def put(self): return await training_service.update_status(self) class TrainingBatchDeleteHandler(BaseHandler): @permission_required("sys:training:delete") @check_demo @operation_log("培训管理", "批量删除") async def post(self): return await training_service.batch_delete(self) ``` **步骤三:注册路由** 在 `src/api/v1/` 下创建路由文件,并在 `router.py` 中注册: ```python # src/api/v1/training.py from modules.training.handlers import ( TrainingAddHandler, TrainingBatchDeleteHandler, TrainingDeleteHandler, TrainingDetailHandler, TrainingPageHandler, TrainingStatusHandler, TrainingUpdateHandler, ) routes = [ (r"/training/page", TrainingPageHandler), (r"/training/detail/(\d+)", TrainingDetailHandler), (r"/training/add", TrainingAddHandler), (r"/training/update", TrainingUpdateHandler), (r"/training/delete/(\d+)", TrainingDeleteHandler), (r"/training/status", TrainingStatusHandler), (r"/training/batchDelete", TrainingBatchDeleteHandler), ] ``` ```python # src/api/v1/router.py from api.v1 import training _RESOURCE_MODULES = [ # ... 其他模块 ... training, ] ``` **步骤四:配置菜单权限** 在数据库 `tornado_menu` 表中插入对应的菜单和权限节点记录。权限标识格式为 `sys:{module}:{action}`,如 `sys:training:add`、`sys:training:page`。 ### 模块解耦 模块之间通过以下方式解耦: | 解耦方式 | 说明 | 示例 | |:---|:---|:---| | **Repository 单例** | 模块级单例,通过 import 引用 | `from modules.user.repository import UserRepository` | | **Service 单例** | 模块级单例,通过 import 引用 | `from user.service import user_service` | | **跨模块引用** | 通过 Repository 引用其他模块的模型 | PositionService 引用 User 模型检查引用 | | **事件机制** | 暂未实现(未来可通过事件总线解耦) | — | ### 复杂模块扩展 对于业务逻辑复杂的模块(如 User、Role、Menu),在继承 `BaseService` 的基础上添加自定义方法: ```python # src/modules/user/service.py class UserService(BaseService(User]): repo = user_repo model = User page_like_fields = ('username', 'realname') page_eq_fields = ('status', 'dept_id') # 自定义方法:用户详情(含角色列表) def get_user_detail(self, user_id): user = self.repo.get_by_id(user_id) if not user: return None data = user.to_dict() data['roleIds'] = get_user_role_ids(user_id) data['roleNames'] = get_user_role_names(user_id) return data # 自定义方法:修改密码 def change_password(self, user_id, old_pwd, new_pwd): user = self.repo.get_by_id(user_id) if not verify_password(old_pwd, user.password): return R.failed("原密码错误") user.password = hash_password(new_pwd) user_repo.create(user) return R.ok(msg="密码修改成功") # 覆盖钩子:新增前哈希密码 def _before_add(self, request, data): data.password = hash_password(data.password or DEFAULT_PASSWORD) # 覆盖钩子:删除前检查 def _before_delete(self, ids): if 1 in parse_id_list(str(ids)): return "超级管理员不能删除" return None ``` ## 二、数据库驱动可切换 ### 驱动注册机制 项目通过 `_SUPPORTED_DRIVERS` 声明支持的驱动,`_normalize_driver()` 处理别名映射和校验,`build_database_url()` 按驱动构建连接串: ```python # src/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.tornado.naivevue') 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_NAME=djangoadmin DB_USER=root DB_PASSWORD=your_password # PostgreSQL DB_DRIVER=postgresql DB_HOST=127.0.0.1 DB_PORT=5432 DB_NAME=djangoadmin DB_USER=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_NAME=djangoadmin DB_USER=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 ``` ## 三、横切关注点扩展 ### Handler 生命周期 Tornado 通过 Handler 生命周期方法实现横切关注点: ```python # src/core/base_handler.py class BaseHandler(tornado.web.RequestHandler): def set_default_headers(self): """设置响应头:CORS + trace_id""" cros_required(self) apply_trace_id(self) @login_required def prepare(self): """请求预处理:JWT 认证""" pass def on_finish(self): """请求结束后:释放数据库会话""" db.close() ``` ### 自定义装饰器 通过 `permission_required` 和 `check_demo` 的模式,可以创建自定义装饰器: ```python # src/common/middleware/operation_log.py def operation_log(module: str, action: str): """操作日志记录装饰器""" def decorator(func): @wraps(func) async def wrapper(self, *args, **kwargs): start = time.time() result = await func(self, *args, **kwargs) duration = time.time() - start save_log(module, action, duration) return result return wrapper return decorator ``` 使用示例: ```python class TrainingAddHandler(BaseHandler): @permission_required("sys:training:add") @check_demo @operation_log("培训管理", "添加") async def post(self): return await training_service.add(self) ``` 装饰器执行顺序(从外到内):`@permission_required` → `@check_demo` → `@operation_log` → 方法体。 ## 四、装饰器扩展 ### 自定义装饰器 通过 `permission_required` 和 `check_demo` 的模式,可以创建自定义装饰器: ```python # src/common/decorators.py # 自定义装饰器示例:操作日志记录 def log_operation(module: str, action: str): """ 操作日志记录装饰器 自动记录接口调用的模块、操作类型、执行耗时 Args: module: 所属模块名称,如 "培训管理" action: 操作类型,如 "新增"、"编辑"、"删除" """ def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): # 记录开始时间,用于计算接口执行耗时 start = time.time() # 执行原业务函数 result = await func(*args, **kwargs) # 计算耗时(秒) duration = time.time() - start # 异步记录操作日志到数据库(包含模块、操作、耗时等信息) save_log(module, action, duration) return result return wrapper return decorator # src/modules/training/handlers.py # 使用示例:多个装饰器叠加使用 # 执行顺序:从外到内(先 permission_required,再 check_demo,再 operation_log) class TrainingAddHandler(BaseHandler): @permission_required("sys:training:add") # 权限校验 @check_demo # 演示环境拦截 @operation_log("培训管理", "添加") # 操作日志记录 async def post(self): """新增培训记录""" return await training_service.add(self) ``` ## 五、配置扩展 ### 环境变量驱动 所有可配置项通过 `.env` 文件管理,新增配置项只需: 1. 在 `.env` 文件中添加变量 2. 在 `src/config/` 对应模块中读取 3. 在业务代码中引用 ```bash # .env CUSTOM_FEATURE_ENABLED=true CUSTOM_TIMEOUT=30 ``` ```python # src/config/custom.py 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. 在数据库 `tornado_menu` 表中配置菜单路由 ### 新增组件 公共组件放在 `ui/src/components/` 目录,遵循 Vue3 组件规范。 ## 扩展性矩阵 | 扩展维度 | 扩展方式 | 修改范围 | 复杂度 | |:---|:---|:---|:---| | 新增 CRUD 模块 | 创建 4 文件 + 注册路由 | 新增文件,不改现有代码 | 低 | | 新增复杂模块 | 继承基类 + 自定义方法 | 新增文件,不改现有代码 | 中 | | 切换数据库驱动 | 修改 .env 配置 | 仅改配置 | 低 | | 新增中间件 | 创建中间件 + 注册 | 新增文件 + 改 app.py | 低 | | 新增装饰器 | 创建装饰器 + 应用 | 新增文件 | 低 | | 新增配置项 | .env + config 模块 | 新增配置 | 低 | | 新增前端页面 | API + Views + Menu | 新增文件 + 改数据库 | 中 | ## 总结 通过分层架构、基类模板方法、装饰器、中间件、环境变量配置等机制,实现了高度的可扩展性。新增 CRUD 模块只需 4 个标准文件 + 1 行路由注册,不修改任何现有代码。数据库驱动通过环境变量切换,中间件通过注册机制插拔。这种设计使得系统在功能持续增长的同时,保持代码结构的清晰和稳定。